Skip to content

Commit 76e8dc3

Browse files
authored
Merge branch 'main' into fix-bazel-vendor-pip-parse
2 parents a410e29 + 036e8c5 commit 76e8dc3

Some content is hidden

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

57 files changed

+373
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ END_UNRELEASED_TEMPLATE
5454

5555
{#v0-0-0-changed}
5656
### Changed
57+
* (gazelle) For package mode, resolve dependencies when imports are relative
58+
to the package path. This is enabled via the
59+
`# gazelle:experimental_allow_relative_imports` true directive ({gh-issue}`2203`).
5760
* (gazelle) Types for exposed members of `python.ParserOutput` are now all public.
5861

5962
{#v0-0-0-fixed}

gazelle/README.md

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,12 @@ gazelle_python_manifest(
121121
requirements = "//:requirements_lock.txt",
122122
# include_stub_packages: bool (default: False)
123123
# If set to True, this flag automatically includes any corresponding type stub packages
124-
# for the third-party libraries that are present and used. For example, if you have
124+
# for the third-party libraries that are present and used. For example, if you have
125125
# `boto3` as a dependency, and this flag is enabled, the corresponding `boto3-stubs`
126126
# package will be automatically included in the BUILD file.
127127
#
128-
# Enabling this feature helps ensure that type hints and stubs are readily available
129-
# for tools like type checkers and IDEs, improving the development experience and
128+
# Enabling this feature helps ensure that type hints and stubs are readily available
129+
# for tools like type checkers and IDEs, improving the development experience and
130130
# reducing manual overhead in managing separate stub packages.
131131
include_stub_packages = True
132132
)
@@ -220,6 +220,8 @@ Python-specific directives are as follows:
220220
| Defines the format of the distribution name in labels to third-party deps. Useful for using Gazelle plugin with other rules with different repository conventions (e.g. `rules_pycross`). Full label is always prepended with (pip) repository name, e.g. `@pip//numpy`. |
221221
| `# gazelle:python_label_normalization` | `snake_case` |
222222
| Controls how distribution names in labels to third-party deps are normalized. Useful for using Gazelle plugin with other rules with different label conventions (e.g. `rules_pycross` uses PEP-503). Can be "snake_case", "none", or "pep503". |
223+
| `# gazelle:experimental_allow_relative_imports` | `false` |
224+
| Controls whether Gazelle resolves dependencies for import statements that use paths relative to the current package. Can be "true" or "false".|
223225

224226
#### Directive: `python_root`:
225227

@@ -468,7 +470,7 @@ def py_test(name, main=None, **kwargs):
468470
name = "__test__",
469471
deps = ["@pip_pytest//:pkg"], # change this to the pytest target in your repo.
470472
)
471-
473+
472474
deps.append(":__test__")
473475
main = ":__test__.py"
474476

@@ -581,6 +583,44 @@ deps = [
581583
]
582584
```
583585

586+
#### Directive: `experimental_allow_relative_imports`
587+
Enables experimental support for resolving relative imports in
588+
`python_generation_mode package`.
589+
590+
By default, when `# gazelle:python_generation_mode package` is enabled,
591+
relative imports (e.g., from .library import foo) are not added to the
592+
deps field of the generated target. This results in incomplete py_library
593+
rules that lack required dependencies on sibling packages.
594+
595+
Example:
596+
Given this Python file import:
597+
```python
598+
from .library import add as _add
599+
from .library import subtract as _subtract
600+
```
601+
602+
Expected BUILD file output:
603+
```starlark
604+
py_library(
605+
name = "py_default_library",
606+
srcs = ["__init__.py"],
607+
deps = [
608+
"//example/library:py_default_library",
609+
],
610+
visibility = ["//visibility:public"],
611+
)
612+
```
613+
614+
Actual output without this annotation:
615+
```starlark
616+
py_library(
617+
name = "py_default_library",
618+
srcs = ["__init__.py"],
619+
visibility = ["//visibility:public"],
620+
)
621+
```
622+
If the directive is set to `true`, gazelle will resolve imports
623+
that are relative to the current package.
584624

585625
### Libraries
586626

gazelle/python/configure.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func (py *Configurer) KnownDirectives() []string {
6868
pythonconfig.TestFilePattern,
6969
pythonconfig.LabelConvention,
7070
pythonconfig.LabelNormalization,
71+
pythonconfig.ExperimentalAllowRelativeImports,
7172
}
7273
}
7374

@@ -222,6 +223,13 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
222223
default:
223224
config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType)
224225
}
226+
case pythonconfig.ExperimentalAllowRelativeImports:
227+
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
228+
if err != nil {
229+
log.Printf("invalid value for gazelle:%s in %q: %q",
230+
pythonconfig.ExperimentalAllowRelativeImports, rel, d.Value)
231+
}
232+
config.SetExperimentalAllowRelativeImports(v)
225233
}
226234
}
227235

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: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,61 @@ 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(mod.From, ".") {
160+
if !cfg.ExperimentalAllowRelativeImports() || !isPackageGeneration {
161+
continue MODULES_LOOP
162+
}
163+
164+
// Count number of leading dots in mod.From (e.g., ".." = 2, "...foo.bar" = 3)
165+
relativeDepth := strings.IndexFunc(mod.From, func(r rune) bool { return r != '.' })
166+
if relativeDepth == -1 {
167+
relativeDepth = len(mod.From)
168+
}
169+
170+
// Extract final symbol (e.g., "some_function") from mod.Name
171+
imported := mod.Name
172+
if idx := strings.LastIndex(mod.Name, "."); idx >= 0 {
173+
imported = mod.Name[idx+1:]
174+
}
175+
176+
// Optional subpath in 'from' clause, e.g. "from ...my_library.foo import x"
177+
fromPath := strings.TrimLeft(mod.From, ".")
178+
var fromParts []string
179+
if fromPath != "" {
180+
fromParts = strings.Split(fromPath, ".")
181+
}
182+
183+
// Current Bazel package as path segments
184+
pkgParts := strings.Split(from.Pkg, "/")
185+
186+
if relativeDepth-1 > len(pkgParts) {
187+
log.Printf("ERROR: Invalid relative import %q in %q: exceeds package root.", mod.Name, mod.Filepath)
188+
continue MODULES_LOOP
189+
}
190+
191+
// Go up relativeDepth - 1 levels
192+
baseParts := pkgParts
193+
if relativeDepth > 1 {
194+
baseParts = pkgParts[:len(pkgParts)-(relativeDepth-1)]
195+
}
196+
// Build absolute module path
197+
absParts := append([]string{}, baseParts...) // base path
198+
absParts = append(absParts, fromParts...) // subpath from 'from'
199+
absParts = append(absParts, imported) // actual imported symbol
200+
201+
moduleName = strings.Join(absParts, ".")
202+
}
203+
204+
moduleParts := strings.Split(moduleName, ".")
205+
possibleModules := []string{moduleName}
157206
for len(moduleParts) > 1 {
158207
// Iterate back through the possible imports until
159208
// a match is found.

gazelle/python/testdata/relative_imports/README.md

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

0 commit comments

Comments
 (0)