Skip to content

feat(gazelle) For package mode, resolve dependencies when imports are relative to the package path #2865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ END_UNRELEASED_TEMPLATE
and will be removed in the next major release.
([#2794](https://github.com/bazel-contrib/rules_python/issues/2794)
* (py_wheel) py_wheel always creates zip64-capable wheel zips
* (gazelle) For package mode, resolve dependencies when imports are relative to the package path. (https://github.com/bazel-contrib/rules_python/issues/2203)

{#v0-0-0-fixed}
### Fixed
Expand Down
8 changes: 8 additions & 0 deletions gazelle/python/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.TestFilePattern,
pythonconfig.LabelConvention,
pythonconfig.LabelNormalization,
pythonconfig.ExperimentalAllowRelativeImports,
}
}

Expand Down Expand Up @@ -222,6 +223,13 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
default:
config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType)
}
case pythonconfig.ExperimentalAllowRelativeImports:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Printf("invalid value for gazelle:%s in %q: %q",
pythonconfig.ExperimentalAllowRelativeImports, rel, d.Value)
}
config.SetExperimentalAllowRelativeImports(v)
}
}

Expand Down
4 changes: 3 additions & 1 deletion gazelle/python/file_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand Down
54 changes: 52 additions & 2 deletions gazelle/python/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,62 @@ 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(mod.From, ".") {
if !cfg.ExperimentalAllowRelativeImports() || !isPackageGeneration {
continue MODULES_LOOP
}

// Count number of leading dots in mod.From (e.g., ".." = 2, "...foo.bar" = 3)
relativeDepth := strings.IndexFunc(mod.From, func(r rune) bool { return r != '.' })
if relativeDepth == -1 {
relativeDepth = len(mod.From)
}

// Extract final symbol (e.g., "some_function") from mod.Name
imported := mod.Name
if idx := strings.LastIndex(mod.Name, "."); idx >= 0 {
imported = mod.Name[idx+1:]
}

// Optional subpath in 'from' clause, e.g. "from ...my_library.foo import x"
fromPath := strings.TrimLeft(mod.From, ".")
var fromParts []string
if fromPath != "" {
fromParts = strings.Split(fromPath, ".")
}

// Current Bazel package as path segments
pkgParts := strings.Split(from.Pkg, "/")

if relativeDepth-1 > len(pkgParts) {
log.Printf("ERROR: Invalid relative import %q in %q: exceeds package root.", mod.Name, mod.Filepath)
continue MODULES_LOOP
}

// Go up relativeDepth - 1 levels
baseParts := pkgParts
if relativeDepth > 1 {
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

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.
Expand Down
4 changes: 0 additions & 4 deletions gazelle/python/testdata/relative_imports/README.md

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# gazelle:python_generation_mode package
# gazelle:experimental_allow_relative_imports true
15 changes: 15 additions & 0 deletions gazelle/python/testdata/relative_imports_package_mode/BUILD.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@rules_python//python:defs.bzl", "py_binary")

# gazelle:python_generation_mode package
# gazelle:experimental_allow_relative_imports true

py_binary(
name = "relative_imports_package_mode_bin",
srcs = ["__main__.py"],
main = "__main__.py",
visibility = ["//:__subpackages__"],
deps = [
"//package1",
"//package2",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from package1.module1 import function1
from package2.module3 import function3

print(function1())
print(function3())
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "package1",
srcs = [
"__init__.py",
"module1.py",
"module2.py",
],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def some_function():
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "my_library",
srcs = ["__init__.py"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "my_library",
srcs = ["__init__.py"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def some_function():
return "some_function"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "foo",
srcs = ["__init__.py"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def some_function():
return "some_function"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "subpackage1",
srcs = [
"__init__.py",
"some_module.py",
],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "subpackage1",
srcs = [
"__init__.py",
"some_module.py",
],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

def some_init():
return "some_init"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

def some_function():
return "some_function"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "subpackage2",
srcs = [
"__init__.py",
"script.py",
],
visibility = ["//:__subpackages__"],
deps = [
"//package1/my_library",
"//package1/my_library/foo",
"//package1/subpackage1",
"//package1/subpackage1/subpackage2/library",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "library",
srcs = ["other_module.py"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ...my_library import (
some_function,
) # Import path should be package1.my_library.some_function
from ...my_library.foo import (
some_function,
) # Import path should be package1.my_library.foo.some_function
from .library import (
other_module,
) # Import path should be package1.subpackage1.subpackage2.library.other_module
from .. import some_module # Import path should be package1.subpackage1.some_module
from .. import some_function # Import path should be package1.subpackage1.some_function
Original file line number Diff line number Diff line change
@@ -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"],
)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "library",
srcs = ["__init__.py"],
visibility = ["//:__subpackages__"],
)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .library import function5


def function3():
return "function3 " + function5()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def function4():
return "function4"
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# gazelle:resolve py resolved_package //package2:resolved_package
# gazelle:python_generation_mode project
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
load("@rules_python//python:defs.bzl", "py_binary", "py_library")

# gazelle:resolve py resolved_package //package2:resolved_package
# gazelle:python_generation_mode project

py_library(
name = "relative_imports",
name = "relative_imports_project_mode",
srcs = [
"package1/module1.py",
"package1/module2.py",
Expand All @@ -12,12 +13,12 @@ py_library(
)

py_binary(
name = "relative_imports_bin",
name = "relative_imports_project_mode_bin",
srcs = ["__main__.py"],
main = "__main__.py",
visibility = ["//:__subpackages__"],
deps = [
":relative_imports",
":relative_imports_project_mode",
"//package2",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Relative imports

This test case asserts that the generated targets handle relative imports in
Python correctly. This tests that if python generation mode is project,
the relative paths are included in the subdirectories.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# This is a test data Bazel workspace.
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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"
Loading