diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad48bee3f..c52481d52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,10 @@ END_UNRELEASED_TEMPLATE ([#2921](https://github.com/bazel-contrib/rules_python/issues/2921)). * (repl) Normalize the path for the `REPL` stub to make it possible to use the default stub template from outside `rules_python` ({gh-issue}`3101`). +* (gazelle) Fixes gazelle adding sibling module dependencies to resolve + absolute imports (Python 2's behavior without `absolute_import`). Previous + behavior can be restored using the directive + `# gazelle:python_resolve_sibling_imports true` {#v0-0-0-added} ### Added diff --git a/gazelle/README.md b/gazelle/README.md index 222c1171ab..8b088a4e70 100644 --- a/gazelle/README.md +++ b/gazelle/README.md @@ -228,6 +228,8 @@ Python-specific directives are as follows: | 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. | | [`# gazelle:python_generate_proto`](#directive-python_generate_proto) | `false` | | 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. | +| `# gazelle:python_resolve_sibling_imports` | `false` | +| Allows absolute imports to be resolved to sibling modules (Python 2's behavior without `absolute_import`). | #### Directive: `python_root`: diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go index 079f1d84d4..13ba6477cd 100644 --- a/gazelle/python/configure.go +++ b/gazelle/python/configure.go @@ -72,6 +72,7 @@ func (py *Configurer) KnownDirectives() []string { pythonconfig.GeneratePyiDeps, pythonconfig.ExperimentalAllowRelativeImports, pythonconfig.GenerateProto, + pythonconfig.PythonResolveSiblingImports, } } @@ -247,6 +248,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { log.Fatal(err) } config.SetGenerateProto(v) + case pythonconfig.PythonResolveSiblingImports: + v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) + if err != nil { + log.Fatal(err) + } + config.SetResolveSiblingImports(v) } } diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index 5b6ba79d69..a180ec527d 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -259,7 +259,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes fqTarget.String(), actualPyBinaryKind, err) continue } - pyBinary := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames). + pyBinary := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addVisibility(visibility). addSrc(filename). addModuleDependencies(mainModules[filename]). @@ -301,7 +301,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes collisionErrors.Add(err) } - pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames). + pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addVisibility(visibility). addSrcs(srcs). addModuleDependencies(allDeps). @@ -354,7 +354,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes collisionErrors.Add(err) } - pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames). + pyBinaryTarget := newTargetBuilder(pyBinaryKind, pyBinaryTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). setMain(pyBinaryEntrypointFilename). addVisibility(visibility). addSrc(pyBinaryEntrypointFilename). @@ -387,7 +387,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes collisionErrors.Add(err) } - conftestTarget := newTargetBuilder(pyLibraryKind, conftestTargetname, pythonProjectRoot, args.Rel, pyFileNames). + conftestTarget := newTargetBuilder(pyLibraryKind, conftestTargetname, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addSrc(conftestFilename). addModuleDependencies(deps). addResolvedDependencies(annotations.includeDeps). @@ -419,7 +419,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes fqTarget.String(), actualPyTestKind, err, pythonconfig.TestNamingConvention) collisionErrors.Add(err) } - return newTargetBuilder(pyTestKind, pyTestTargetName, pythonProjectRoot, args.Rel, pyFileNames). + return newTargetBuilder(pyTestKind, pyTestTargetName, pythonProjectRoot, args.Rel, pyFileNames, cfg.ResolveSiblingImports()). addSrcs(srcs). addModuleDependencies(deps). addResolvedDependencies(annotations.includeDeps). @@ -476,7 +476,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes for _, pyTestTarget := range pyTestTargets { if conftest != nil { - conftestModule := Module{Name: strings.TrimSuffix(conftestFilename, ".py")} + conftestModule := Module{Name: importSpecFromSrc(pythonProjectRoot, args.Rel, conftestFilename).Imp} if pyTestTarget.annotations.includePytestConftest == nil { // unset; default behavior pyTestTarget.addModuleDependency(conftestModule) @@ -605,7 +605,7 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config pyProtoLibraryName = ruleName } - pyProtoLibrary := newTargetBuilder(pyProtoLibraryKind, pyProtoLibraryName, pythonProjectRoot, args.Rel, &emptySiblings). + pyProtoLibrary := newTargetBuilder(pyProtoLibraryKind, pyProtoLibraryName, pythonProjectRoot, args.Rel, &emptySiblings, false). addVisibility(visibility). addResolvedDependency(":" + protoRuleName). generateImportsAttribute().build() @@ -622,7 +622,7 @@ func generateProtoLibraries(args language.GenerateArgs, cfg *pythonconfig.Config continue } - emptyRule := newTargetBuilder(pyProtoLibraryKind, ruleName, pythonProjectRoot, args.Rel, &emptySiblings).build() + emptyRule := newTargetBuilder(pyProtoLibraryKind, ruleName, pythonProjectRoot, args.Rel, &emptySiblings, false).build() res.Empty = append(res.Empty, emptyRule) } diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index 0dd80841d4..cc57180a49 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -164,8 +164,6 @@ func (py *Resolver) Resolve( modules := modulesRaw.(*treeset.Set) it := modules.Iterator() explainDependency := os.Getenv("EXPLAIN_DEPENDENCY") - // Resolve relative paths for package generation - isPackageGeneration := !cfg.PerFileGeneration() && !cfg.CoarseGrainedGeneration() hasFatalError := false MODULES_LOOP: for it.Next() { @@ -173,7 +171,7 @@ func (py *Resolver) Resolve( moduleName := mod.Name // Transform relative imports `.` or `..foo.bar` into the package path from root. if strings.HasPrefix(mod.From, ".") { - if !cfg.ExperimentalAllowRelativeImports() || !isPackageGeneration { + if !cfg.ExperimentalAllowRelativeImports() { continue MODULES_LOOP } @@ -210,9 +208,9 @@ func (py *Resolver) Resolve( baseParts = pkgParts[:len(pkgParts)-(relativeDepth-1)] } // Build absolute module path - absParts := append([]string{}, baseParts...) // base path - absParts = append(absParts, fromParts...) // subpath from 'from' - absParts = append(absParts, imported) // actual imported symbol + absParts := append([]string{}, baseParts...) // base path + absParts = append(absParts, fromParts...) // subpath from 'from' + absParts = append(absParts, imported) // actual imported symbol moduleName = strings.Join(absParts, ".") } diff --git a/gazelle/python/target.go b/gazelle/python/target.go index 6e6c3f4b14..3fe5819e00 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -25,34 +25,36 @@ import ( // targetBuilder builds targets to be generated by Gazelle. type targetBuilder struct { - kind string - name string - pythonProjectRoot string - bzlPackage string - srcs *treeset.Set - siblingSrcs *treeset.Set - deps *treeset.Set - resolvedDeps *treeset.Set - visibility *treeset.Set - main *string - imports []string - testonly bool - annotations *annotations + kind string + name string + pythonProjectRoot string + bzlPackage string + srcs *treeset.Set + siblingSrcs *treeset.Set + deps *treeset.Set + resolvedDeps *treeset.Set + visibility *treeset.Set + main *string + imports []string + testonly bool + annotations *annotations + resolveSiblingImports bool } // newTargetBuilder constructs a new targetBuilder. -func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingSrcs *treeset.Set) *targetBuilder { +func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingSrcs *treeset.Set, resolveSiblingImports bool) *targetBuilder { return &targetBuilder{ - kind: kind, - name: name, - pythonProjectRoot: pythonProjectRoot, - bzlPackage: bzlPackage, - srcs: treeset.NewWith(godsutils.StringComparator), - siblingSrcs: siblingSrcs, - deps: treeset.NewWith(moduleComparator), - resolvedDeps: treeset.NewWith(godsutils.StringComparator), - visibility: treeset.NewWith(godsutils.StringComparator), - annotations: new(annotations), + kind: kind, + name: name, + pythonProjectRoot: pythonProjectRoot, + bzlPackage: bzlPackage, + srcs: treeset.NewWith(godsutils.StringComparator), + siblingSrcs: siblingSrcs, + deps: treeset.NewWith(moduleComparator), + resolvedDeps: treeset.NewWith(godsutils.StringComparator), + visibility: treeset.NewWith(godsutils.StringComparator), + annotations: new(annotations), + resolveSiblingImports: resolveSiblingImports, } } @@ -77,7 +79,7 @@ func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { if dep.From != "" { fileName = dep.From + ".py" } - if t.siblingSrcs.Contains(fileName) && fileName != filepath.Base(dep.Filepath) { + if t.resolveSiblingImports && t.siblingSrcs.Contains(fileName) && fileName != filepath.Base(dep.Filepath) { // importing another module from the same package, converting to absolute imports to make // dependency resolution easier dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp @@ -138,7 +140,6 @@ func (t *targetBuilder) setAnnotations(val annotations) *targetBuilder { return t } - // generateImportsAttribute generates the imports attribute. // These are a list of import directories to be added to the PYTHONPATH. In our // case, the value we add is on Bazel sub-packages to be able to perform imports diff --git a/gazelle/python/testdata/annotation_include_dep/BUILD.in b/gazelle/python/testdata/annotation_include_dep/BUILD.in index af2c2cea4b..5131712aca 100644 --- a/gazelle/python/testdata/annotation_include_dep/BUILD.in +++ b/gazelle/python/testdata/annotation_include_dep/BUILD.in @@ -1 +1,2 @@ # gazelle:python_generation_mode file +# gazelle:python_resolve_sibling_imports true diff --git a/gazelle/python/testdata/annotation_include_dep/BUILD.out b/gazelle/python/testdata/annotation_include_dep/BUILD.out index 1cff8f4676..412bf456f5 100644 --- a/gazelle/python/testdata/annotation_include_dep/BUILD.out +++ b/gazelle/python/testdata/annotation_include_dep/BUILD.out @@ -1,6 +1,7 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") # gazelle:python_generation_mode file +# gazelle:python_resolve_sibling_imports true py_library( name = "__init__", diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.in b/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.in index e69de29bb2..5c25b0d5a6 100644 --- a/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.in +++ b/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_resolve_sibling_imports true diff --git a/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.out b/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.out index 60695352ca..52b915208e 100644 --- a/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.out +++ b/gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.out @@ -1,5 +1,7 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") +# gazelle:python_resolve_sibling_imports true + py_binary( name = "binary", srcs = ["binary.py"], diff --git a/gazelle/python/testdata/naming_convention/BUILD.in b/gazelle/python/testdata/naming_convention/BUILD.in index 7517848a92..fee53ba7ff 100644 --- a/gazelle/python/testdata/naming_convention/BUILD.in +++ b/gazelle/python/testdata/naming_convention/BUILD.in @@ -1,3 +1,4 @@ # gazelle:python_library_naming_convention my_$package_name$_library # gazelle:python_binary_naming_convention my_$package_name$_binary # gazelle:python_test_naming_convention my_$package_name$_test +# gazelle:python_resolve_sibling_imports true diff --git a/gazelle/python/testdata/naming_convention/BUILD.out b/gazelle/python/testdata/naming_convention/BUILD.out index e2f067489c..7392cfeb35 100644 --- a/gazelle/python/testdata/naming_convention/BUILD.out +++ b/gazelle/python/testdata/naming_convention/BUILD.out @@ -3,6 +3,7 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") # gazelle:python_library_naming_convention my_$package_name$_library # gazelle:python_binary_naming_convention my_$package_name$_binary # gazelle:python_test_naming_convention my_$package_name$_test +# gazelle:python_resolve_sibling_imports true py_library( name = "my_naming_convention_library", diff --git a/gazelle/python/testdata/sibling_imports/README.md b/gazelle/python/testdata/sibling_imports/README.md index e59be07634..d21a671b1c 100644 --- a/gazelle/python/testdata/sibling_imports/README.md +++ b/gazelle/python/testdata/sibling_imports/README.md @@ -1,3 +1,13 @@ # Sibling imports -This test case asserts that imports from sibling modules are resolved correctly. It covers 3 different types of imports in `pkg/unit_test.py` \ No newline at end of file +This test case asserts that imports from sibling modules are resolved correctly +when the `python_resolve_sibling_imports` directive is enabled (default +behavior). It covers 3 different types of imports in `pkg/unit_test.py`: + +- `import a` - resolves to the sibling `a.py` in the same package +- `import test_util` - resolves to the sibling `test_util.py` in the same + package +- `from b import run` - resolves to the sibling `b.py` in the same package + +When sibling imports are enabled, we allow them to be satisfied by sibling +modules (ie. modules in the same package). diff --git a/gazelle/python/testdata/sibling_imports/pkg/BUILD.in b/gazelle/python/testdata/sibling_imports/pkg/BUILD.in index e69de29bb2..5c25b0d5a6 100644 --- a/gazelle/python/testdata/sibling_imports/pkg/BUILD.in +++ b/gazelle/python/testdata/sibling_imports/pkg/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_resolve_sibling_imports true diff --git a/gazelle/python/testdata/sibling_imports/pkg/BUILD.out b/gazelle/python/testdata/sibling_imports/pkg/BUILD.out index cae6c3f17a..e8c13098c2 100644 --- a/gazelle/python/testdata/sibling_imports/pkg/BUILD.out +++ b/gazelle/python/testdata/sibling_imports/pkg/BUILD.out @@ -1,5 +1,7 @@ load("@rules_python//python:defs.bzl", "py_library", "py_test") +# gazelle:python_resolve_sibling_imports true + py_library( name = "pkg", srcs = [ @@ -23,4 +25,3 @@ py_test( ":test_util", ], ) - diff --git a/gazelle/python/testdata/sibling_imports_disabled/BUILD.in b/gazelle/python/testdata/sibling_imports_disabled/BUILD.in new file mode 100644 index 0000000000..9509fd9727 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_resolve_sibling_imports false +# gazelle:experimental_allow_relative_imports true diff --git a/gazelle/python/testdata/sibling_imports_disabled/BUILD.out b/gazelle/python/testdata/sibling_imports_disabled/BUILD.out new file mode 100644 index 0000000000..7568f38f50 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/BUILD.out @@ -0,0 +1,18 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_resolve_sibling_imports false +# gazelle:experimental_allow_relative_imports true + +py_library( + name = "sibling_imports_disabled", + srcs = [ + "a.py", + "b.py", + ], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "test_util", + srcs = ["test_util.py"], +) diff --git a/gazelle/python/testdata/sibling_imports_disabled/README.md b/gazelle/python/testdata/sibling_imports_disabled/README.md new file mode 100644 index 0000000000..d534a44bf1 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/README.md @@ -0,0 +1,22 @@ +# Sibling imports disabled + +This test case asserts that imports from sibling modules are NOT resolved as +absolute imports when the `python_resolve_sibling_imports` directive is +disabled. It covers different types of imports in `pkg/unit_test.py`: + +- `import a` - resolves to the root-level `a.py` instead of the sibling + `pkg/a.py` +- `from typing import Iterable` - resolves to the stdlib `typing` module + (not the sibling `typing.py`). +- `from .b import run` / `from .typing import A` - resolves to the sibling + `pkg/b.py` / `pkg/typing.py` (with + `gazelle:experimental_allow_relative_imports` enabled) +- `import test_util` - resolves to the root-level `test_util.py` instead of + the sibling `pkg/test_util.py` +- `from b import run` - resolves to the root-level `b.py` instead of the + sibling `pkg/b.py` + +When sibling imports are disabled with +`# gazelle:python_resolve_sibling_imports false`, the imports remain as-is +and follow standard Python resolution rules where absolute imports can't refer +to sibling modules. diff --git a/gazelle/python/testdata/sibling_imports_disabled/WORKSPACE b/gazelle/python/testdata/sibling_imports_disabled/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/sibling_imports_disabled/a.py b/gazelle/python/testdata/sibling_imports_disabled/a.py new file mode 100644 index 0000000000..fad4fb1ff9 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/a.py @@ -0,0 +1 @@ +# Root level a.py file for testing disabled sibling imports diff --git a/gazelle/python/testdata/sibling_imports_disabled/b.py b/gazelle/python/testdata/sibling_imports_disabled/b.py new file mode 100644 index 0000000000..a5eafc436f --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/b.py @@ -0,0 +1,3 @@ +# Root level b.py file for testing disabled sibling imports +def run(): + pass diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/BUILD.in b/gazelle/python/testdata/sibling_imports_disabled/pkg/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/BUILD.out b/gazelle/python/testdata/sibling_imports_disabled/pkg/BUILD.out new file mode 100644 index 0000000000..e778ce1076 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/pkg/BUILD.out @@ -0,0 +1,27 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +py_library( + name = "pkg", + srcs = [ + "__init__.py", + "a.py", + "b.py", + "typing.py", + ], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "test_util", + srcs = ["test_util.py"], + deps = [":pkg"], +) + +py_test( + name = "unit_test", + srcs = ["unit_test.py"], + deps = [ + "//:sibling_imports_disabled", + "//:test_util", + ], +) diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/__init__.py b/gazelle/python/testdata/sibling_imports_disabled/pkg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/a.py b/gazelle/python/testdata/sibling_imports_disabled/pkg/a.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/b.py b/gazelle/python/testdata/sibling_imports_disabled/pkg/b.py new file mode 100644 index 0000000000..d04d423678 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/pkg/b.py @@ -0,0 +1,2 @@ +def run(): + pass diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/test_util.py b/gazelle/python/testdata/sibling_imports_disabled/pkg/test_util.py new file mode 100644 index 0000000000..01cc15da86 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/pkg/test_util.py @@ -0,0 +1,2 @@ +from .b import run +from .typing import A diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/typing.py b/gazelle/python/testdata/sibling_imports_disabled/pkg/typing.py new file mode 100644 index 0000000000..76f516f79f --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/pkg/typing.py @@ -0,0 +1 @@ +A = 1 diff --git a/gazelle/python/testdata/sibling_imports_disabled/pkg/unit_test.py b/gazelle/python/testdata/sibling_imports_disabled/pkg/unit_test.py new file mode 100644 index 0000000000..3c551a1cb1 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/pkg/unit_test.py @@ -0,0 +1,5 @@ +from typing import Iterable + +import a +import test_util +from b import run diff --git a/gazelle/python/testdata/sibling_imports_disabled/test.yaml b/gazelle/python/testdata/sibling_imports_disabled/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/sibling_imports_disabled/test_util.py b/gazelle/python/testdata/sibling_imports_disabled/test_util.py new file mode 100644 index 0000000000..f5fa1b34ea --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled/test_util.py @@ -0,0 +1 @@ +# Root level test_util.py file for testing disabled sibling imports diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/BUILD.in b/gazelle/python/testdata/sibling_imports_disabled_file_mode/BUILD.in new file mode 100644 index 0000000000..04494394c7 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/BUILD.in @@ -0,0 +1,3 @@ +# gazelle:python_generation_mode file +# gazelle:python_resolve_sibling_imports false +# gazelle:experimental_allow_relative_imports true diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/BUILD.out b/gazelle/python/testdata/sibling_imports_disabled_file_mode/BUILD.out new file mode 100644 index 0000000000..da53e14864 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/BUILD.out @@ -0,0 +1,22 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_generation_mode file +# gazelle:python_resolve_sibling_imports false +# gazelle:experimental_allow_relative_imports true + +py_library( + name = "a", + srcs = ["a.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "b", + srcs = ["b.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "test_util", + srcs = ["test_util.py"], +) diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/README.md b/gazelle/python/testdata/sibling_imports_disabled_file_mode/README.md new file mode 100644 index 0000000000..0bfbcffb58 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/README.md @@ -0,0 +1,22 @@ +# Sibling imports disabled (file generation mode) + +This test case asserts that imports from sibling modules are NOT resolved as +absolute imports when the `python_resolve_sibling_imports` directive is +disabled. It covers different types of imports in `pkg/unit_test.py`: + +- `import a` - resolves to the root-level `a.py` instead of the sibling + `pkg/a.py` +- `from typing import Iterable` - resolves to the stdlib `typing` module + (not the sibling `typing.py`). +- `from .b import run` / `from .typing import A` - resolves to the sibling + `pkg/b.py` / `pkg/typing.py` (with + `gazelle:experimental_allow_relative_imports` enabled) +- `import test_util` - resolves to the root-level `test_util.py` instead of + the sibling `pkg/test_util.py` +- `from b import run` - resolves to the root-level `b.py` instead of the + sibling `pkg/b.py` + +When sibling imports are disabled with +`# gazelle:python_resolve_sibling_imports false`, the imports remain as-is +and follow standard Python resolution rules where absolute imports can't refer +to sibling modules. diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/WORKSPACE b/gazelle/python/testdata/sibling_imports_disabled_file_mode/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/a.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/a.py new file mode 100644 index 0000000000..fad4fb1ff9 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/a.py @@ -0,0 +1 @@ +# Root level a.py file for testing disabled sibling imports diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/b.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/b.py new file mode 100644 index 0000000000..a5eafc436f --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/b.py @@ -0,0 +1,3 @@ +# Root level b.py file for testing disabled sibling imports +def run(): + pass diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/BUILD.in b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/BUILD.out b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/BUILD.out new file mode 100644 index 0000000000..ab161e135f --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/BUILD.out @@ -0,0 +1,38 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +py_library( + name = "a", + srcs = ["a.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "b", + srcs = ["b.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "typing", + srcs = ["typing.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "test_util", + srcs = ["test_util.py"], + deps = [ + ":b", + ":typing", + ], +) + +py_test( + name = "unit_test", + srcs = ["unit_test.py"], + deps = [ + "//:a", + "//:b", + "//:test_util", + ], +) diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/__init__.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/a.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/a.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/b.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/b.py new file mode 100644 index 0000000000..d04d423678 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/b.py @@ -0,0 +1,2 @@ +def run(): + pass diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/test_util.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/test_util.py new file mode 100644 index 0000000000..01cc15da86 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/test_util.py @@ -0,0 +1,2 @@ +from .b import run +from .typing import A diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/typing.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/typing.py new file mode 100644 index 0000000000..76f516f79f --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/typing.py @@ -0,0 +1 @@ +A = 1 diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/unit_test.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/unit_test.py new file mode 100644 index 0000000000..3c551a1cb1 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/pkg/unit_test.py @@ -0,0 +1,5 @@ +from typing import Iterable + +import a +import test_util +from b import run diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/test.yaml b/gazelle/python/testdata/sibling_imports_disabled_file_mode/test.yaml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/test.yaml @@ -0,0 +1 @@ +--- diff --git a/gazelle/python/testdata/sibling_imports_disabled_file_mode/test_util.py b/gazelle/python/testdata/sibling_imports_disabled_file_mode/test_util.py new file mode 100644 index 0000000000..f5fa1b34ea --- /dev/null +++ b/gazelle/python/testdata/sibling_imports_disabled_file_mode/test_util.py @@ -0,0 +1 @@ +# Root level test_util.py file for testing disabled sibling imports diff --git a/gazelle/python/testdata/simple_test_with_conftest/BUILD.in b/gazelle/python/testdata/simple_test_with_conftest/BUILD.in index 3f2beb3147..6dfab75442 100644 --- a/gazelle/python/testdata/simple_test_with_conftest/BUILD.in +++ b/gazelle/python/testdata/simple_test_with_conftest/BUILD.in @@ -1 +1,3 @@ load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_resolve_sibling_imports true diff --git a/gazelle/python/testdata/simple_test_with_conftest/BUILD.out b/gazelle/python/testdata/simple_test_with_conftest/BUILD.out index 18079bf2f4..62e1c550e6 100644 --- a/gazelle/python/testdata/simple_test_with_conftest/BUILD.out +++ b/gazelle/python/testdata/simple_test_with_conftest/BUILD.out @@ -1,5 +1,7 @@ load("@rules_python//python:defs.bzl", "py_library", "py_test") +# gazelle:python_resolve_sibling_imports true + py_library( name = "simple_test_with_conftest", srcs = [ diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/BUILD.in b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/BUILD.in new file mode 100644 index 0000000000..f8a40fe26c --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/BUILD.in @@ -0,0 +1,3 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_resolve_sibling_imports false diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/BUILD.out b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/BUILD.out new file mode 100644 index 0000000000..b5a7066aff --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/BUILD.out @@ -0,0 +1,29 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +# gazelle:python_resolve_sibling_imports false + +py_library( + name = "simple_test_with_conftest_sibling_imports_disabled", + srcs = [ + "__init__.py", + "foo.py", + ], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "simple_test_with_conftest_sibling_imports_disabled_test", + srcs = ["__test__.py"], + main = "__test__.py", + deps = [ + ":conftest", + ":simple_test_with_conftest_sibling_imports_disabled", + ], +) diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/README.md b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/README.md new file mode 100644 index 0000000000..98793c23de --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/README.md @@ -0,0 +1,4 @@ +# Simple test with conftest.py (sibling imports disable) + +This test case asserts that a simple `py_test` is generated as expected when a +`conftest.py` is present with sibling imports disabled. diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/WORKSPACE b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/__init__.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/__init__.py new file mode 100644 index 0000000000..6a49193fe4 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/__init__.py @@ -0,0 +1,3 @@ +from foo import foo + +_ = foo diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/__test__.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/__test__.py new file mode 100644 index 0000000000..d6085a41b4 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/__test__.py @@ -0,0 +1,12 @@ +import unittest + +from __init__ import foo + + +class FooTest(unittest.TestCase): + def test_foo(self): + self.assertEqual("foo", foo()) + + +if __name__ == "__main__": + unittest.main() diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.in b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.in new file mode 100644 index 0000000000..3f2beb3147 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.in @@ -0,0 +1 @@ +load("@rules_python//python:defs.bzl", "py_library") diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out new file mode 100644 index 0000000000..ef8591f199 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/BUILD.out @@ -0,0 +1,27 @@ +load("@rules_python//python:defs.bzl", "py_library", "py_test") + +py_library( + name = "bar", + srcs = [ + "__init__.py", + "bar.py", + ], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "conftest", + testonly = True, + srcs = ["conftest.py"], + visibility = ["//:__subpackages__"], +) + +py_test( + name = "bar_test", + srcs = ["__test__.py"], + main = "__test__.py", + deps = [ + ":conftest", + "//:simple_test_with_conftest_sibling_imports_disabled", + ], +) diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/__init__.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/__init__.py new file mode 100644 index 0000000000..0c59205559 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/__init__.py @@ -0,0 +1,3 @@ +from bar import bar + +_ = bar diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/__test__.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/__test__.py new file mode 100644 index 0000000000..c3d4734eed --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/__test__.py @@ -0,0 +1,12 @@ +import unittest + +from __init__ import bar + + +class BarTest(unittest.TestCase): + def test_bar(self): + self.assertEqual("bar", bar()) + + +if __name__ == "__main__": + unittest.main() diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/bar.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/bar.py new file mode 100644 index 0000000000..ee70a51f03 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/bar.py @@ -0,0 +1,2 @@ +def bar(): + return "bar" diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/conftest.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/bar/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/conftest.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/conftest.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/foo.py b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/foo.py new file mode 100644 index 0000000000..cf68624419 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/foo.py @@ -0,0 +1,2 @@ +def foo(): + return "foo" diff --git a/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/test.yaml b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/test.yaml new file mode 100644 index 0000000000..8071ef4094 --- /dev/null +++ b/gazelle/python/testdata/simple_test_with_conftest_sibling_imports_disabled/test.yaml @@ -0,0 +1,4 @@ + +--- +expect: + exit_code: 0 diff --git a/gazelle/python/testdata/subdir_sources/BUILD.in b/gazelle/python/testdata/subdir_sources/BUILD.in index adfdefdc8a..e8f3827bd2 100644 --- a/gazelle/python/testdata/subdir_sources/BUILD.in +++ b/gazelle/python/testdata/subdir_sources/BUILD.in @@ -1 +1,2 @@ # gazelle:python_generation_mode project +# gazelle:python_resolve_sibling_imports true diff --git a/gazelle/python/testdata/subdir_sources/BUILD.out b/gazelle/python/testdata/subdir_sources/BUILD.out index 5d77890d4f..5b96ad7576 100644 --- a/gazelle/python/testdata/subdir_sources/BUILD.out +++ b/gazelle/python/testdata/subdir_sources/BUILD.out @@ -2,6 +2,7 @@ load("@rules_python//python:defs.bzl", "py_binary") # gazelle:python_generation_mode project +# gazelle:python_resolve_sibling_imports true py_binary( name = "subdir_sources_bin", diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index 001fd334a4..b3d56591ee 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -107,6 +107,11 @@ const ( // GenerateProto represents the directive that controls whether to generate // python_generate_proto targets. GenerateProto = "python_generate_proto" + // PythonResolveSiblingImports represents the directive that controls whether + // absolute imports can be solved to sibling modules. When enabled, imports + // like "import a" can be resolved to sibling modules. When disabled, they + // can only be resolved as an absolute import. + PythonResolveSiblingImports = "python_resolve_sibling_imports" ) // GenerationModeType represents one of the generation modes for the Python @@ -198,6 +203,7 @@ type Config struct { experimentalAllowRelativeImports bool generatePyiDeps bool generateProto bool + resolveSiblingImports bool } type LabelNormalizationType int @@ -237,6 +243,7 @@ func New( experimentalAllowRelativeImports: false, generatePyiDeps: false, generateProto: false, + resolveSiblingImports: false, } } @@ -273,6 +280,7 @@ func (c *Config) NewChild() *Config { experimentalAllowRelativeImports: c.experimentalAllowRelativeImports, generatePyiDeps: c.generatePyiDeps, generateProto: c.generateProto, + resolveSiblingImports: c.resolveSiblingImports, } } @@ -592,6 +600,16 @@ func (c *Config) GenerateProto() bool { return c.generateProto } +// SetResolveSiblingImports sets whether absolute imports can be resolved to sibling modules. +func (c *Config) SetResolveSiblingImports(resolveSiblingImports bool) { + c.resolveSiblingImports = resolveSiblingImports +} + +// ResolveSiblingImports returns whether absolute imports can be resolved to sibling modules. +func (c *Config) ResolveSiblingImports() bool { + return c.resolveSiblingImports +} + // FormatThirdPartyDependency returns a label to a third-party dependency performing all formating and normalization. func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionName string) label.Label { conventionalDistributionName := strings.ReplaceAll(c.labelConvention, distributionNameLabelConventionSubstitution, distributionName)