Skip to content

Commit ab3e3f7

Browse files
authored
fix(gazelle): Do not resolve absolute imports to sibling modules (bazel-contrib#3106)
Currently, gazelle allows absolute imports to be resolved to sibling modules: an `import foo` statement will resolve to a `foo.py` file in the same folder if such file exists. This seems to be a Python 2 behavior (ie. pre-`from __future__ import absolute_import`), and doesn't work on the current rules_python setup. This behavior is explicitly tested in the [siblings_import](https://github.com/bazel-contrib/rules_python/tree/cbe6d38d01c14de46d90ea717d0f2090117533fa/gazelle/python/testdata/sibling_imports) test case. However, recreating the exact same repository layout from this test case and running `bazel test //pkg:unit_test`, the test fails with the import failing. This PR adds a new directive, `gazelle:python_resolve_sibling_imports`, to allow disabling such behavior. The actual changes are in 3 places: - In `gazelle/python/target.go`, the directive is added to `if t.siblingSrcs.Contains(fileName) && fileName != filepath.Base(dep.Filepath)`, which is where the import is converted to a full absolute import if it matches a sibling file; - In `gazelle/python/generate.go`, the handling of `conftest.py` was dependent on this behavior (ie. it added a dependency on the module `conftest`, assuming that it would be resolved to the relative module). That was modified to compute the full absolute module path instead. - In `gazelle/python/resolve.go`, resolve relative imports even when using file generation mode. I also explicitly added `gazelle:python_resolve_sibling_imports true` to any test that breaks if the default value of this directive is changed to `false`.
1 parent e6bba92 commit ab3e3f7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+388
-42
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ END_UNRELEASED_TEMPLATE
9595
([#2921](https://github.com/bazel-contrib/rules_python/issues/2921)).
9696
* (repl) Normalize the path for the `REPL` stub to make it possible to use the
9797
default stub template from outside `rules_python` ({gh-issue}`3101`).
98+
* (gazelle) Fixes gazelle adding sibling module dependencies to resolve
99+
absolute imports (Python 2's behavior without `absolute_import`). Previous
100+
behavior can be restored using the directive
101+
`# gazelle:python_resolve_sibling_imports true`
98102

99103
{#v0-0-0-added}
100104
### Added

gazelle/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ Python-specific directives are as follows:
228228
| Controls whether to generate a separate `pyi_deps` attribute for type-checking dependencies or merge them into the regular `deps` attribute. When `false` (default), type-checking dependencies are merged into `deps` for backward compatibility. When `true`, generates separate `pyi_deps`. Imports in blocks with the format `if typing.TYPE_CHECKING:`/`if TYPE_CHECKING:` and type-only stub packages (eg. boto3-stubs) are recognized as type-checking dependencies. |
229229
| [`# gazelle:python_generate_proto`](#directive-python_generate_proto) | `false` |
230230
| Controls whether to generate a `py_proto_library` for each `proto_library` in the package. By default we load this rule from the `@protobuf` repository; use `gazelle:map_kind` if you need to load this from somewhere else. |
231+
| `# gazelle:python_resolve_sibling_imports` | `false` |
232+
| Allows absolute imports to be resolved to sibling modules (Python 2's behavior without `absolute_import`). |
231233

232234
#### Directive: `python_root`:
233235

gazelle/python/configure.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func (py *Configurer) KnownDirectives() []string {
7272
pythonconfig.GeneratePyiDeps,
7373
pythonconfig.ExperimentalAllowRelativeImports,
7474
pythonconfig.GenerateProto,
75+
pythonconfig.PythonResolveSiblingImports,
7576
}
7677
}
7778

@@ -247,6 +248,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
247248
log.Fatal(err)
248249
}
249250
config.SetGenerateProto(v)
251+
case pythonconfig.PythonResolveSiblingImports:
252+
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
253+
if err != nil {
254+
log.Fatal(err)
255+
}
256+
config.SetResolveSiblingImports(v)
250257
}
251258
}
252259

gazelle/python/generate.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
259259
fqTarget.String(), actualPyBinaryKind, err)
260260
continue
261261
}
262-
pyBinary := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames).
262+
pyBinary := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()).
263263
addVisibility(visibility).
264264
addSrc(filename).
265265
addModuleDependencies(mainModules[filename]).
@@ -301,7 +301,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
301301
collisionErrors.Add(err)
302302
}
303303

304-
pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames).
304+
pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()).
305305
addVisibility(visibility).
306306
addSrcs(srcs).
307307
addModuleDependencies(allDeps).
@@ -354,7 +354,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
354354
collisionErrors.Add(err)
355355
}
356356

357-
pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames).
357+
pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()).
358358
setMain(pyBinaryEntrypointFilename).
359359
addVisibility(visibility).
360360
addSrc(pyBinaryEntrypointFilename).
@@ -387,7 +387,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
387387
collisionErrors.Add(err)
388388
}
389389

390-
conftestTarget := newTargetBuilder(pyLibraryKind, conftestTargetname, pythonProjectRoot, args.Rel, pyFileNames).
390+
conftestTarget := newTargetBuilder(pyLibraryKind, conftestTargetname, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()).
391391
addSrc(conftestFilename).
392392
addModuleDependencies(deps).
393393
addResolvedDependencies(annotations.includeDeps).
@@ -419,7 +419,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
419419
fqTarget.String(), actualPyTestKind, err, pythonconfig.TestNamingConvention)
420420
collisionErrors.Add(err)
421421
}
422-
return newTargetBuilder(pyTestKind, pyTestTargetName, pythonProjectRoot, args.Rel, pyFileNames).
422+
return newTargetBuilder(pyTestKind, pyTestTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()).
423423
addSrcs(srcs).
424424
addModuleDependencies(deps).
425425
addResolvedDependencies(annotations.includeDeps).
@@ -476,7 +476,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
476476

477477
for _, pyTestTarget := range pyTestTargets {
478478
if conftest != nil {
479-
conftestModule := Module{Name: strings.TrimSuffix(conftestFilename, ".py")}
479+
conftestModule := Module{Name: importSpecFromSrc(pythonProjectRoot, args.Rel, conftestFilename).Imp}
480480
if pyTestTarget.annotations.includePytestConftest == nil {
481481
// unset; default behavior
482482
pyTestTarget.addModuleDependency(conftestModule)
@@ -605,7 +605,7 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config
605605
pyProtoLibraryName = ruleName
606606
}
607607

608-
pyProtoLibrary := newTargetBuilder(pyProtoLibraryKind, pyProtoLibraryName, pythonProjectRoot, args.Rel, &emptySiblings).
608+
pyProtoLibrary := newTargetBuilder(pyProtoLibraryKind, pyProtoLibraryName, pythonProjectRoot, args.Rel, &emptySiblings, false).
609609
addVisibility(visibility).
610610
addResolvedDependency(":" + protoRuleName).
611611
generateImportsAttribute().build()
@@ -622,7 +622,7 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config
622622
continue
623623
}
624624

625-
emptyRule := newTargetBuilder(pyProtoLibraryKind, ruleName, pythonProjectRoot, args.Rel, &emptySiblings).build()
625+
emptyRule := newTargetBuilder(pyProtoLibraryKind, ruleName, pythonProjectRoot, args.Rel, &emptySiblings, false).build()
626626
res.Empty = append(res.Empty, emptyRule)
627627
}
628628

gazelle/python/resolve.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,14 @@ func (py *Resolver) Resolve(
164164
modules := modulesRaw.(*treeset.Set)
165165
it := modules.Iterator()
166166
explainDependency := os.Getenv("EXPLAIN_DEPENDENCY")
167-
// Resolve relative paths for package generation
168-
isPackageGeneration := !cfg.PerFileGeneration() && !cfg.CoarseGrainedGeneration()
169167
hasFatalError := false
170168
MODULES_LOOP:
171169
for it.Next() {
172170
mod := it.Value().(Module)
173171
moduleName := mod.Name
174172
// Transform relative imports `.` or `..foo.bar` into the package path from root.
175173
if strings.HasPrefix(mod.From, ".") {
176-
if !cfg.ExperimentalAllowRelativeImports() || !isPackageGeneration {
174+
if !cfg.ExperimentalAllowRelativeImports() {
177175
continue MODULES_LOOP
178176
}
179177

@@ -210,9 +208,9 @@ func (py *Resolver) Resolve(
210208
baseParts = pkgParts[:len(pkgParts)-(relativeDepth-1)]
211209
}
212210
// Build absolute module path
213-
absParts := append([]string{}, baseParts...) // base path
214-
absParts = append(absParts, fromParts...) // subpath from 'from'
215-
absParts = append(absParts, imported) // actual imported symbol
211+
absParts := append([]string{}, baseParts...) // base path
212+
absParts = append(absParts, fromParts...) // subpath from 'from'
213+
absParts = append(absParts, imported) // actual imported symbol
216214

217215
moduleName = strings.Join(absParts, ".")
218216
}

gazelle/python/target.go

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,34 +25,36 @@ import (
2525

2626
// targetBuilder builds targets to be generated by Gazelle.
2727
type targetBuilder struct {
28-
kind string
29-
name string
30-
pythonProjectRoot string
31-
bzlPackage string
32-
srcs *treeset.Set
33-
siblingSrcs *treeset.Set
34-
deps *treeset.Set
35-
resolvedDeps *treeset.Set
36-
visibility *treeset.Set
37-
main *string
38-
imports []string
39-
testonly bool
40-
annotations *annotations
28+
kind string
29+
name string
30+
pythonProjectRoot string
31+
bzlPackage string
32+
srcs *treeset.Set
33+
siblingSrcs *treeset.Set
34+
deps *treeset.Set
35+
resolvedDeps *treeset.Set
36+
visibility *treeset.Set
37+
main *string
38+
imports []string
39+
testonly bool
40+
annotations *annotations
41+
resolveSiblingImports bool
4142
}
4243

4344
// newTargetBuilder constructs a new targetBuilder.
44-
func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingSrcs *treeset.Set) *targetBuilder {
45+
func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingSrcs *treeset.Set, resolveSiblingImports bool) *targetBuilder {
4546
return &targetBuilder{
46-
kind: kind,
47-
name: name,
48-
pythonProjectRoot: pythonProjectRoot,
49-
bzlPackage: bzlPackage,
50-
srcs: treeset.NewWith(godsutils.StringComparator),
51-
siblingSrcs: siblingSrcs,
52-
deps: treeset.NewWith(moduleComparator),
53-
resolvedDeps: treeset.NewWith(godsutils.StringComparator),
54-
visibility: treeset.NewWith(godsutils.StringComparator),
55-
annotations: new(annotations),
47+
kind: kind,
48+
name: name,
49+
pythonProjectRoot: pythonProjectRoot,
50+
bzlPackage: bzlPackage,
51+
srcs: treeset.NewWith(godsutils.StringComparator),
52+
siblingSrcs: siblingSrcs,
53+
deps: treeset.NewWith(moduleComparator),
54+
resolvedDeps: treeset.NewWith(godsutils.StringComparator),
55+
visibility: treeset.NewWith(godsutils.StringComparator),
56+
annotations: new(annotations),
57+
resolveSiblingImports: resolveSiblingImports,
5658
}
5759
}
5860

@@ -77,7 +79,7 @@ func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder {
7779
if dep.From != "" {
7880
fileName = dep.From + ".py"
7981
}
80-
if t.siblingSrcs.Contains(fileName) && fileName != filepath.Base(dep.Filepath) {
82+
if t.resolveSiblingImports && t.siblingSrcs.Contains(fileName) && fileName != filepath.Base(dep.Filepath) {
8183
// importing another module from the same package, converting to absolute imports to make
8284
// dependency resolution easier
8385
dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp
@@ -138,7 +140,6 @@ func (t *targetBuilder) setAnnotations(val annotations) *targetBuilder {
138140
return t
139141
}
140142

141-
142143
// generateImportsAttribute generates the imports attribute.
143144
// These are a list of import directories to be added to the PYTHONPATH. In our
144145
// case, the value we add is on Bazel sub-packages to be able to perform imports
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
# gazelle:python_generation_mode file
2+
# gazelle:python_resolve_sibling_imports true

gazelle/python/testdata/annotation_include_dep/BUILD.out

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
22

33
# gazelle:python_generation_mode file
4+
# gazelle:python_resolve_sibling_imports true
45

56
py_library(
67
name = "__init__",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# gazelle:python_resolve_sibling_imports true

gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.out

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
22

3+
# gazelle:python_resolve_sibling_imports true
4+
35
py_binary(
46
name = "binary",
57
srcs = ["binary.py"],

0 commit comments

Comments
 (0)