Skip to content

Commit 847fdae

Browse files
committed
Address comments and add directive
Update
1 parent 38103ef commit 847fdae

File tree

10 files changed

+95
-21
lines changed

10 files changed

+95
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ END_UNRELEASED_TEMPLATE
7272
* (py_wheel) py_wheel always creates zip64-capable wheel zips
7373
* (providers) (experimental) {obj}`PyInfo.venv_symlinks` replaces
7474
`PyInfo.site_packages_symlinks`
75+
* (gazelle) For package mode, resolve dependencies when imports are relative
76+
to the package path. This is enabled via the
77+
`# gazelle:experimental_allow_relative_imports` true directive.
78+
(https://github.com/bazel-contrib/rules_python/issues/2203)
7579

7680
{#v0-0-0-fixed}
7781
### 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+
#### Annotation: `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/resolve.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ func (py *Resolver) Imports(c *config.Config, r *rule.Rule, f *rule.File) []reso
7171
}
7272
pythonProjectRoot := cfg.PythonProjectRoot()
7373
provide := importSpecFromSrc(pythonProjectRoot, f.Pkg, src)
74-
// log.Printf("import to index: %v", provide.Imp)
7574
provides = append(provides, provide)
7675
}
7776
if len(provides) == 0 {
@@ -158,18 +157,14 @@ func (py *Resolver) Resolve(
158157
moduleName := mod.Name
159158
// Transform relative imports `.` or `..foo.bar` into the package path from root.
160159
if strings.HasPrefix(mod.From, ".") {
161-
if !isPackageGeneration {
160+
if !cfg.ExperimentalAllowRelativeImports() || !isPackageGeneration {
162161
continue MODULES_LOOP
163162
}
164163

165164
// Count number of leading dots in mod.From (e.g., ".." = 2, "...foo.bar" = 3)
166-
relativeDepth := 0
167-
for i := 0; i < len(mod.From); i++ {
168-
if mod.From[i] == '.' {
169-
relativeDepth++
170-
} else {
171-
break
172-
}
165+
relativeDepth := strings.IndexFunc(mod.From, func(r rune) bool { return r != '.' })
166+
if relativeDepth == -1 {
167+
relativeDepth = len(mod.From)
173168
}
174169

175170
// Extract final symbol (e.g., "some_function") from mod.Name
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
# gazelle:python_generation_mode package
2+
# gazelle:experimental_allow_relative_imports true

gazelle/python/testdata/relative_imports_package_mode/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")
22

33
# gazelle:python_generation_mode package
4+
# gazelle:experimental_allow_relative_imports true
45

56
py_binary(
67
name = "relative_imports_package_mode_bin",
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# Resolve deps for relative imports
22

3-
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.
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.
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
2-
from ...my_library import some_function # Import path should be package1.my_library.some_function
3-
from ...my_library.foo import some_function # Import path should be package1.my_library.foo.some_function
4-
from .library import other_module # Import path should be package1.subpackage1.subpackage2.library.other_module
5-
from .. import some_module # Import path should be package1.subpackage1.some_module
6-
from .. import some_function # Import path should be package1.subpackage1.some_function
1+
from ...my_library import (
2+
some_function,
3+
) # Import path should be package1.my_library.some_function
4+
from ...my_library.foo import (
5+
some_function,
6+
) # Import path should be package1.my_library.foo.some_function
7+
from .library import (
8+
other_module,
9+
) # Import path should be package1.subpackage1.subpackage2.library.other_module
10+
from .. import some_module # Import path should be package1.subpackage1.some_module
11+
from .. import some_function # Import path should be package1.subpackage1.some_function
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Relative imports
22

33
This test case asserts that the generated targets handle relative imports in
4-
Python correctly. This tests that if python generation mode is project, the relative paths are included in the subdirectories.
4+
Python correctly. This tests that if python generation mode is project,
5+
the relative paths are included in the subdirectories.

gazelle/pythonconfig/pythonconfig.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ const (
9191
// names of labels to third-party dependencies are normalized. Supported values
9292
// are 'none', 'pep503' and 'snake_case' (default). See LabelNormalizationType.
9393
LabelNormalization = "python_label_normalization"
94+
// ExperimentalAllowRelativeImports represents the directive that controls
95+
// whether relative imports are allowed.
96+
ExperimentalAllowRelativeImports = "experimental_allow_relative_imports"
9497
)
9598

9699
// GenerationModeType represents one of the generation modes for the Python
@@ -177,6 +180,7 @@ type Config struct {
177180
testFilePattern []string
178181
labelConvention string
179182
labelNormalization LabelNormalizationType
183+
experimentalAllowRelativeImports bool
180184
}
181185

182186
type LabelNormalizationType int
@@ -212,6 +216,7 @@ func New(
212216
testFilePattern: strings.Split(DefaultTestFilePatternString, ","),
213217
labelConvention: DefaultLabelConvention,
214218
labelNormalization: DefaultLabelNormalizationType,
219+
experimentalAllowRelativeImports: false,
215220
}
216221
}
217222

@@ -244,6 +249,7 @@ func (c *Config) NewChild() *Config {
244249
testFilePattern: c.testFilePattern,
245250
labelConvention: c.labelConvention,
246251
labelNormalization: c.labelNormalization,
252+
experimentalAllowRelativeImports: c.experimentalAllowRelativeImports,
247253
}
248254
}
249255

@@ -520,6 +526,16 @@ func (c *Config) LabelNormalization() LabelNormalizationType {
520526
return c.labelNormalization
521527
}
522528

529+
// SetExperimentalAllowRelativeImports sets whether relative imports are allowed.
530+
func (c *Config) SetExperimentalAllowRelativeImports(allowRelativeImports bool) {
531+
c.experimentalAllowRelativeImports = allowRelativeImports
532+
}
533+
534+
// ExperimentalAllowRelativeImports returns whether relative imports are allowed.
535+
func (c *Config) ExperimentalAllowRelativeImports() bool {
536+
return c.experimentalAllowRelativeImports
537+
}
538+
523539
// FormatThirdPartyDependency returns a label to a third-party dependency performing all formating and normalization.
524540
func (c *Config) FormatThirdPartyDependency(repositoryName string, distributionName string) label.Label {
525541
conventionalDistributionName := strings.ReplaceAll(c.labelConvention, distributionNameLabelConventionSubstitution, distributionName)

0 commit comments

Comments
 (0)