Skip to content

Commit aecbe48

Browse files
committed
Resolve dependencies when imports are relative to the package path
1 parent 9f3512f commit aecbe48

File tree

35 files changed

+203
-10
lines changed

35 files changed

+203
-10
lines changed

gazelle/python/file_parser.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool {
165165
}
166166
} else if node.Type() == sitterNodeTypeImportFromStatement {
167167
from := node.Child(1).Content(p.code)
168-
if strings.HasPrefix(from, ".") {
168+
// If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1.
169+
// If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules.
170+
if from == "." {
169171
return true
170172
}
171173
for j := 3; j < int(node.ChildCount()); j++ {

gazelle/python/resolve.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,56 @@ func (py *Resolver) Resolve(
148148
modules := modulesRaw.(*treeset.Set)
149149
it := modules.Iterator()
150150
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
151+
// Resolve relative paths for package generation
152+
isPackageGeneration := !cfg.PerFileGeneration() && !cfg.CoarseGrainedGeneration()
151153
hasFatalError := false
152154
MODULES_LOOP:
153155
for it.Next() {
154156
mod := it.Value().(module)
155-
moduleParts := strings.Split(mod.Name, ".")
156-
possibleModules := []string{mod.Name}
157+
moduleName := mod.Name
158+
// Transform relative imports `.` or `..foo.bar` into the package path from root.
159+
if strings.HasPrefix(moduleName, ".") {
160+
// If not package generation mode, skip relative imports
161+
if !isPackageGeneration {
162+
continue MODULES_LOOP
163+
}
164+
relativeDepth := 0
165+
for i := 0; i < len(moduleName); i++ {
166+
if moduleName[i] == '.' {
167+
relativeDepth++
168+
} else {
169+
break
170+
}
171+
}
172+
173+
// Extract suffix after leading dots
174+
relativeSuffix := moduleName[relativeDepth:]
175+
var relativeSuffixParts []string
176+
if relativeSuffix != "" {
177+
relativeSuffixParts = strings.Split(relativeSuffix, ".")
178+
}
179+
180+
// Split current package label into parts
181+
pkgParts := strings.Split(from.Pkg, "/")
182+
183+
if relativeDepth- 1 > len(pkgParts) {
184+
// Trying to go above the root
185+
log.Printf("ERROR: Invalid relative import %q in %q: exceeds package root.", moduleName, mod.Filepath)
186+
continue MODULES_LOOP
187+
}
188+
189+
// Go up `relativeDepth - 1` levels
190+
baseParts := pkgParts
191+
if relativeDepth > 1 {
192+
baseParts = pkgParts[:len(pkgParts)-(relativeDepth-1)]
193+
}
194+
195+
absParts := append(baseParts, relativeSuffixParts...)
196+
moduleName = strings.Join(absParts, ".")
197+
}
198+
199+
moduleParts := strings.Split(moduleName, ".")
200+
possibleModules := []string{moduleName}
157201
for len(moduleParts) > 1 {
158202
// Iterate back through the possible imports until
159203
// a match is found.

gazelle/python/testdata/relative_imports/README.md

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# gazelle:python_generation_mode package
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
load("@rules_python//python:defs.bzl", "py_binary")
2+
3+
# gazelle:python_generation_mode package
4+
5+
py_binary(
6+
name = "relative_imports_package_mode_bin",
7+
srcs = ["__main__.py"],
8+
main = "__main__.py",
9+
visibility = ["//:__subpackages__"],
10+
deps = [
11+
"//package1",
12+
"//package2",
13+
],
14+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Resolve deps for relative imports
2+
3+
This test case verifies that the generated targets correctly handle relative imports in Python. Specifically, when the Python generation mode is set to "package," it ensures that relative import statements such as from .foo import X are properly resolved to their corresponding modules.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from package1.module1 import function1
2+
from package2.module3 import function3
3+
4+
print(function1())
5+
print(function3())
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
load("@rules_python//python:defs.bzl", "py_library")
2+
3+
py_library(
4+
name = "package1",
5+
srcs = [
6+
"module1.py",
7+
"module2.py",
8+
],
9+
visibility = ["//:__subpackages__"],
10+
)

0 commit comments

Comments
 (0)