diff --git a/gazelle/python/file_parser.go b/gazelle/python/file_parser.go index c147984fc3..06b63be989 100644 --- a/gazelle/python/file_parser.go +++ b/gazelle/python/file_parser.go @@ -165,7 +165,9 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { } } else if node.Type() == sitterNodeTypeImportFromStatement { from := node.Child(1).Content(p.code) - if strings.HasPrefix(from, ".") { + // If the import is from the current package, we don't need to add it to the modules i.e. from . import Class1. + // If the import is from a different relative package i.e. from .package1 import foo, we need to add it to the modules. + if from == "." { return true } for j := 3; j < int(node.ChildCount()); j++ { diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index 7a2ec3d68a..9c6da483a9 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -148,12 +148,56 @@ 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() { mod := it.Value().(module) - moduleParts := strings.Split(mod.Name, ".") - possibleModules := []string{mod.Name} + moduleName := mod.Name + // Transform relative imports `.` or `..foo.bar` into the package path from root. + if strings.HasPrefix(moduleName, ".") { + // If not package generation mode, skip relative imports + if !isPackageGeneration { + continue MODULES_LOOP + } + relativeDepth := 0 + for i := 0; i < len(moduleName); i++ { + if moduleName[i] == '.' { + relativeDepth++ + } else { + break + } + } + + // Extract suffix after leading dots + relativeSuffix := moduleName[relativeDepth:] + var relativeSuffixParts []string + if relativeSuffix != "" { + relativeSuffixParts = strings.Split(relativeSuffix, ".") + } + + // Split current package label into parts + pkgParts := strings.Split(from.Pkg, "/") + + if relativeDepth- 1 > len(pkgParts) { + // Trying to go above the root + log.Printf("ERROR: Invalid relative import %q in %q: exceeds package root.", moduleName, mod.Filepath) + continue MODULES_LOOP + } + + // Go up `relativeDepth - 1` levels + baseParts := pkgParts + if relativeDepth > 1 { + baseParts = pkgParts[:len(pkgParts)-(relativeDepth-1)] + } + + absParts := append(baseParts, relativeSuffixParts...) + moduleName = strings.Join(absParts, ".") + } + + moduleParts := strings.Split(moduleName, ".") + possibleModules := []string{moduleName} for len(moduleParts) > 1 { // Iterate back through the possible imports until // a match is found. diff --git a/gazelle/python/testdata/relative_imports/BUILD.out b/gazelle/python/testdata/relative_imports/BUILD.out index bf9524480a..e63d921c5b 100644 --- a/gazelle/python/testdata/relative_imports/BUILD.out +++ b/gazelle/python/testdata/relative_imports/BUILD.out @@ -1,23 +1,14 @@ -load("@rules_python//python:defs.bzl", "py_binary", "py_library") +load("@rules_python//python:defs.bzl", "py_binary") # gazelle:resolve py resolved_package //package2:resolved_package -py_library( - name = "relative_imports", - srcs = [ - "package1/module1.py", - "package1/module2.py", - ], - visibility = ["//:__subpackages__"], -) - py_binary( name = "relative_imports_bin", srcs = ["__main__.py"], main = "__main__.py", visibility = ["//:__subpackages__"], deps = [ - ":relative_imports", + "//package1", "//package2", ], ) diff --git a/gazelle/python/testdata/relative_imports/package1/BUILD.in b/gazelle/python/testdata/relative_imports/package1/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports/package1/BUILD.out b/gazelle/python/testdata/relative_imports/package1/BUILD.out new file mode 100644 index 0000000000..6667417598 --- /dev/null +++ b/gazelle/python/testdata/relative_imports/package1/BUILD.out @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "package1", + srcs = [ + "module1.py", + "module2.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/relative_imports/package2/BUILD.out b/gazelle/python/testdata/relative_imports/package2/BUILD.out index 3e03e75f9b..6064891a73 100644 --- a/gazelle/python/testdata/relative_imports/package2/BUILD.out +++ b/gazelle/python/testdata/relative_imports/package2/BUILD.out @@ -6,8 +6,10 @@ py_library( "__init__.py", "module3.py", "module4.py", - "subpackage1/module5.py", ], visibility = ["//:__subpackages__"], - deps = [":resolved_package"], + deps = [ + ":resolved_package", + "//package2/subpackage1", + ], ) diff --git a/gazelle/python/testdata/relative_imports/package2/subpackage1/BUILD.in b/gazelle/python/testdata/relative_imports/package2/subpackage1/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/relative_imports/package2/subpackage1/BUILD.out b/gazelle/python/testdata/relative_imports/package2/subpackage1/BUILD.out new file mode 100644 index 0000000000..2eaff46179 --- /dev/null +++ b/gazelle/python/testdata/relative_imports/package2/subpackage1/BUILD.out @@ -0,0 +1,8 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "subpackage1", + srcs = ["module5.py"], + visibility = ["//:__subpackages__"], + deps = ["//package2"], +) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/BUILD.in b/gazelle/python/testdata/resolve_deps_relative_imports/BUILD.in new file mode 100644 index 0000000000..421b48688a --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_generation_mode package diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/BUILD.out b/gazelle/python/testdata/resolve_deps_relative_imports/BUILD.out new file mode 100644 index 0000000000..96aa75b778 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/BUILD.out @@ -0,0 +1,14 @@ +load("@rules_python//python:defs.bzl", "py_binary") + +# gazelle:python_generation_mode package + +py_binary( + name = "resolve_deps_relative_imports_bin", + srcs = ["__main__.py"], + main = "__main__.py", + visibility = ["//:__subpackages__"], + deps = [ + "//package1", + "//package2", + ], +) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/README.md b/gazelle/python/testdata/resolve_deps_relative_imports/README.md new file mode 100644 index 0000000000..01c3fe79c6 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/README.md @@ -0,0 +1,3 @@ +# Resolve deps for relative imports + +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. diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/WORKSPACE b/gazelle/python/testdata/resolve_deps_relative_imports/WORKSPACE new file mode 100644 index 0000000000..4959898cdd --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/WORKSPACE @@ -0,0 +1 @@ +# This is a test data Bazel workspace. diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/__main__.py b/gazelle/python/testdata/resolve_deps_relative_imports/__main__.py new file mode 100644 index 0000000000..4fb887a803 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/__main__.py @@ -0,0 +1,5 @@ +from package1.module1 import function1 +from package2.module3 import function3 + +print(function1()) +print(function3()) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package1/BUILD.in b/gazelle/python/testdata/resolve_deps_relative_imports/package1/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package1/BUILD.out b/gazelle/python/testdata/resolve_deps_relative_imports/package1/BUILD.out new file mode 100644 index 0000000000..6667417598 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package1/BUILD.out @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "package1", + srcs = [ + "module1.py", + "module2.py", + ], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package1/module1.py b/gazelle/python/testdata/resolve_deps_relative_imports/package1/module1.py new file mode 100644 index 0000000000..28502f1f84 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package1/module1.py @@ -0,0 +1,19 @@ +# Copyright 2023 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .module2 import function2 + + +def function1(): + return "function1 " + function2() diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package1/module2.py b/gazelle/python/testdata/resolve_deps_relative_imports/package1/module2.py new file mode 100644 index 0000000000..0cbc5f0be0 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package1/module2.py @@ -0,0 +1,17 @@ +# Copyright 2023 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def function2(): + return "function2" diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/BUILD.in b/gazelle/python/testdata/resolve_deps_relative_imports/package2/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/BUILD.out b/gazelle/python/testdata/resolve_deps_relative_imports/package2/BUILD.out new file mode 100644 index 0000000000..bd78108159 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package2/BUILD.out @@ -0,0 +1,12 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "package2", + srcs = [ + "__init__.py", + "module3.py", + "module4.py", + ], + visibility = ["//:__subpackages__"], + deps = ["//package2/library"], +) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/__init__.py b/gazelle/python/testdata/resolve_deps_relative_imports/package2/__init__.py new file mode 100644 index 0000000000..3d19d80e21 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package2/__init__.py @@ -0,0 +1,20 @@ +from .library import add as _add +from .library import divide as _divide +from .library import multiply as _multiply +from .library import subtract as _subtract + + +def add(a, b): + return _add(a, b) + + +def divide(a, b): + return _divide(a, b) + + +def multiply(a, b): + return _multiply(a, b) + + +def subtract(a, b): + return _subtract(a, b) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/BUILD.in b/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/BUILD.out b/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/BUILD.out new file mode 100644 index 0000000000..d704b7fe93 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "library", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/__init__.py b/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/__init__.py new file mode 100644 index 0000000000..5f8fc62492 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package2/library/__init__.py @@ -0,0 +1,14 @@ +def add(a, b): + return a + b + + +def divide(a, b): + return a / b + + +def multiply(a, b): + return a * b + + +def subtract(a, b): + return a - b diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/module3.py b/gazelle/python/testdata/resolve_deps_relative_imports/package2/module3.py new file mode 100644 index 0000000000..6b955cfda6 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package2/module3.py @@ -0,0 +1,5 @@ +from .library import function5 + + +def function3(): + return "function3 " + function5() diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/package2/module4.py b/gazelle/python/testdata/resolve_deps_relative_imports/package2/module4.py new file mode 100644 index 0000000000..6e69699985 --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/package2/module4.py @@ -0,0 +1,2 @@ +def function4(): + return "function4" diff --git a/gazelle/python/testdata/resolve_deps_relative_imports/test.yaml b/gazelle/python/testdata/resolve_deps_relative_imports/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/resolve_deps_relative_imports/test.yaml @@ -0,0 +1,15 @@ +# Copyright 2023 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +---