From 41297f8261552bf0c867f2864f7979fe45be9d3f Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Thu, 19 Jun 2025 17:19:55 -0700 Subject: [PATCH 1/9] gazelle: Add support for generating pyi_deps --- CHANGELOG.md | 2 + gazelle/python/file_parser.go | 45 +++++++++++++++++-- gazelle/python/file_parser_test.go | 37 +++++++++++++++ gazelle/python/parser.go | 2 + gazelle/python/resolve.go | 42 ++++++++++++++--- gazelle/python/target.go | 30 +++++++++++-- .../testdata/add_type_stub_packages/BUILD.out | 6 ++- .../testdata/type_checking_imports/BUILD.in | 1 + .../testdata/type_checking_imports/BUILD.out | 25 +++++++++++ .../testdata/type_checking_imports/README.md | 5 +++ .../testdata/type_checking_imports/WORKSPACE | 1 + .../testdata/type_checking_imports/bar.py | 9 ++++ .../testdata/type_checking_imports/baz.py | 15 +++++++ .../testdata/type_checking_imports/foo.py | 20 +++++++++ .../type_checking_imports/gazelle_python.yaml | 19 ++++++++ .../testdata/type_checking_imports/test.yaml | 15 +++++++ 16 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 gazelle/python/testdata/type_checking_imports/BUILD.in create mode 100644 gazelle/python/testdata/type_checking_imports/BUILD.out create mode 100644 gazelle/python/testdata/type_checking_imports/README.md create mode 100644 gazelle/python/testdata/type_checking_imports/WORKSPACE create mode 100644 gazelle/python/testdata/type_checking_imports/bar.py create mode 100644 gazelle/python/testdata/type_checking_imports/baz.py create mode 100644 gazelle/python/testdata/type_checking_imports/foo.py create mode 100644 gazelle/python/testdata/type_checking_imports/gazelle_python.yaml create mode 100644 gazelle/python/testdata/type_checking_imports/test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 9897dc9ec8..22d3a76b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed * (gazelle) Types for exposed members of `python.ParserOutput` are now all public. +* (gazelle) Dependencies added to satisfy type-only imports (`if TYPE_CHECKING`) and type stub packages are + now added to `pyi_deps` instead of `deps`. {#v0-0-0-fixed} ### Fixed diff --git a/gazelle/python/file_parser.go b/gazelle/python/file_parser.go index 3f8363fbdf..907ebd86c4 100644 --- a/gazelle/python/file_parser.go +++ b/gazelle/python/file_parser.go @@ -47,9 +47,10 @@ type ParserOutput struct { } type FileParser struct { - code []byte - relFilepath string - output ParserOutput + code []byte + relFilepath string + output ParserOutput + inTypeCheckingBlock bool } func NewFileParser() *FileParser { @@ -158,6 +159,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { continue } m.Filepath = p.relFilepath + m.TypeCheckingOnly = p.inTypeCheckingBlock if strings.HasPrefix(m.Name, ".") { continue } @@ -176,6 +178,7 @@ func (p *FileParser) parseImportStatements(node *sitter.Node) bool { m.Filepath = p.relFilepath m.From = from m.Name = fmt.Sprintf("%s.%s", from, m.Name) + m.TypeCheckingOnly = p.inTypeCheckingBlock p.output.Modules = append(p.output.Modules, m) } } else { @@ -200,10 +203,43 @@ func (p *FileParser) SetCodeAndFile(code []byte, relPackagePath, filename string p.output.FileName = filename } +// isTypeCheckingBlock returns true if the given node is an `if TYPE_CHECKING:` block. +func (p *FileParser) isTypeCheckingBlock(node *sitter.Node) bool { + if node.Type() != sitterNodeTypeIfStatement || node.ChildCount() < 2 { + return false + } + + condition := node.Child(1) + + // Handle `if TYPE_CHECKING:` + if condition.Type() == sitterNodeTypeIdentifier && condition.Content(p.code) == "TYPE_CHECKING" { + return true + } + + // Handle `if typing.TYPE_CHECKING:` + if condition.Type() == "attribute" && condition.ChildCount() >= 3 { + object := condition.Child(0) + attr := condition.Child(2) + if object.Type() == sitterNodeTypeIdentifier && object.Content(p.code) == "typing" && + attr.Type() == sitterNodeTypeIdentifier && attr.Content(p.code) == "TYPE_CHECKING" { + return true + } + } + + return false +} + func (p *FileParser) parse(ctx context.Context, node *sitter.Node) { if node == nil { return } + + // Check if this is a TYPE_CHECKING block + wasInTypeCheckingBlock := p.inTypeCheckingBlock + if p.isTypeCheckingBlock(node) { + p.inTypeCheckingBlock = true + } + for i := 0; i < int(node.ChildCount()); i++ { if err := ctx.Err(); err != nil { return @@ -217,6 +253,9 @@ func (p *FileParser) parse(ctx context.Context, node *sitter.Node) { } p.parse(ctx, child) } + + // Restore the previous state + p.inTypeCheckingBlock = wasInTypeCheckingBlock } func (p *FileParser) Parse(ctx context.Context) (*ParserOutput, error) { diff --git a/gazelle/python/file_parser_test.go b/gazelle/python/file_parser_test.go index 20085f0e76..f4db1a316b 100644 --- a/gazelle/python/file_parser_test.go +++ b/gazelle/python/file_parser_test.go @@ -254,3 +254,40 @@ func TestParseFull(t *testing.T) { FileName: "a.py", }, *output) } + +func TestTypeCheckingImports(t *testing.T) { + code := ` +import sys +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import boto3 + from rest_framework import serializers + +def example_function(): + _ = sys.version_info +` + p := NewFileParser() + p.SetCodeAndFile([]byte(code), "", "test.py") + + result, err := p.Parse(context.Background()) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + + // Check that we found the expected modules + expectedModules := map[string]bool{ + "sys": false, + "typing.TYPE_CHECKING": false, + "boto3": true, + "rest_framework.serializers": true, + } + + for _, mod := range result.Modules { + if expected, exists := expectedModules[mod.Name]; exists { + if mod.TypeCheckingOnly != expected { + t.Errorf("Module %s: expected TypeCheckingOnly=%v, got %v", mod.Name, expected, mod.TypeCheckingOnly) + } + } + } +} diff --git a/gazelle/python/parser.go b/gazelle/python/parser.go index cf80578220..163a806619 100644 --- a/gazelle/python/parser.go +++ b/gazelle/python/parser.go @@ -158,6 +158,8 @@ type Module struct { // If this was a from import, e.g. from foo import bar, From indicates the module // from which it is imported. From string `json:"from"` + // Whether this import is type-checking only (inside if TYPE_CHECKING block). + TypeCheckingOnly bool `json:"type_checking_only"` } // moduleComparator compares modules by name. diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index 996cbbadc0..a4be080b56 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -39,6 +39,9 @@ const ( // resolvedDepsKey is the attribute key used to pass dependencies that don't // need to be resolved by the dependency resolver in the Resolver step. resolvedDepsKey = "_gazelle_python_resolved_deps" + // resolvedPyiDepsKey is the attribute key used to pass type-checking dependencies that don't + // need to be resolved by the dependency resolver in the Resolver step. + resolvedPyiDepsKey = "_gazelle_python_resolved_pyi_deps" ) // Resolver satisfies the resolve.Resolver interface. It resolves dependencies @@ -123,6 +126,16 @@ func (py *Resolver) Embeds(r *rule.Rule, from label.Label) []label.Label { return make([]label.Label, 0) } +// addDependency adds a dependency to either the regular deps or pyiDeps set based on +// whether the module is type-checking only. +func addDependency(dep string, mod Module, deps, pyiDeps *treeset.Set) { + if mod.TypeCheckingOnly { + pyiDeps.Add(dep) + } else { + deps.Add(dep) + } +} + // Resolve translates imported libraries for a given rule into Bazel // dependencies. Information about imported libraries is returned for each // rule generated by language.GenerateRules in @@ -141,6 +154,8 @@ func (py *Resolver) Resolve( // join with the main Gazelle binary with other rules. It may conflict with // other generators that generate py_* targets. deps := treeset.NewWith(godsutils.StringComparator) + pyiDeps := treeset.NewWith(godsutils.StringComparator) + if modulesRaw != nil { cfgs := c.Exts[languageName].(pythonconfig.Configs) cfg := cfgs[from.Pkg] @@ -179,7 +194,7 @@ func (py *Resolver) Resolve( override.Repo = "" } dep := override.Rel(from.Repo, from.Pkg).String() - deps.Add(dep) + addDependency(dep, mod, deps, pyiDeps) if explainDependency == dep { log.Printf("Explaining dependency (%s): "+ "in the target %q, the file %q imports %q at line %d, "+ @@ -190,7 +205,7 @@ func (py *Resolver) Resolve( } } else { if dep, distributionName, ok := cfg.FindThirdPartyDependency(moduleName); ok { - deps.Add(dep) + addDependency(dep, mod, deps, pyiDeps) // Add the type and stub dependencies if they exist. modules := []string{ fmt.Sprintf("%s_stubs", strings.ToLower(distributionName)), @@ -200,7 +215,8 @@ func (py *Resolver) Resolve( } for _, module := range modules { if dep, _, ok := cfg.FindThirdPartyDependency(module); ok { - deps.Add(dep) + // Type stub packages always go to pyiDeps + pyiDeps.Add(dep) } } if explainDependency == dep { @@ -259,7 +275,7 @@ func (py *Resolver) Resolve( } matchLabel := filteredMatches[0].Label.Rel(from.Repo, from.Pkg) dep := matchLabel.String() - deps.Add(dep) + addDependency(dep, mod, deps, pyiDeps) if explainDependency == dep { log.Printf("Explaining dependency (%s): "+ "in the target %q, the file %q imports %q at line %d, "+ @@ -284,15 +300,29 @@ func (py *Resolver) Resolve( os.Exit(1) } } - resolvedDeps := r.PrivateAttr(resolvedDepsKey).(*treeset.Set) + + addResolvedDepsAndSetAttr(r, deps, resolvedDepsKey, "deps") + addResolvedDepsAndSetAttr(r, pyiDeps, resolvedPyiDepsKey, "pyi_deps") +} + +// addResolvedDepsAndSetAttr adds the pre-resolved dependencies from the rule's private attributes +// to the provided deps set and sets the attribute on the rule. +func addResolvedDepsAndSetAttr( + r *rule.Rule, + deps *treeset.Set, + resolvedDepsAttrName string, + depsAttrName string, +) { + resolvedDeps := r.PrivateAttr(resolvedDepsAttrName).(*treeset.Set) if !resolvedDeps.Empty() { it := resolvedDeps.Iterator() for it.Next() { deps.Add(it.Value()) } } + if !deps.Empty() { - r.SetAttr("deps", convertDependencySetToExpr(deps)) + r.SetAttr(depsAttrName, convertDependencySetToExpr(deps)) } } diff --git a/gazelle/python/target.go b/gazelle/python/target.go index 1fb9218656..326ac2172a 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -15,11 +15,12 @@ package python import ( + "path/filepath" + "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/rule" "github.com/emirpasic/gods/sets/treeset" godsutils "github.com/emirpasic/gods/utils" - "path/filepath" ) // targetBuilder builds targets to be generated by Gazelle. @@ -31,7 +32,9 @@ type targetBuilder struct { srcs *treeset.Set siblingSrcs *treeset.Set deps *treeset.Set + pyiDeps *treeset.Set resolvedDeps *treeset.Set + resolvedPyiDeps *treeset.Set visibility *treeset.Set main *string imports []string @@ -48,7 +51,9 @@ func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingS srcs: treeset.NewWith(godsutils.StringComparator), siblingSrcs: siblingSrcs, deps: treeset.NewWith(moduleComparator), + pyiDeps: treeset.NewWith(moduleComparator), resolvedDeps: treeset.NewWith(godsutils.StringComparator), + resolvedPyiDeps: treeset.NewWith(godsutils.StringComparator), visibility: treeset.NewWith(godsutils.StringComparator), } } @@ -79,7 +84,13 @@ func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { // dependency resolution easier dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp } - t.deps.Add(dep) + + // Add to appropriate dependency set based on whether it's type-checking only + if dep.TypeCheckingOnly { + t.pyiDeps.Add(dep) + } else { + t.deps.Add(dep) + } return t } @@ -162,12 +173,23 @@ func (t *targetBuilder) build() *rule.Rule { if t.imports != nil { r.SetAttr("imports", t.imports) } - if !t.deps.Empty() { - r.SetPrivateAttr(config.GazelleImportsKey, t.deps) + if combinedDeps := t.combinedDeps(); !combinedDeps.Empty() { + r.SetPrivateAttr(config.GazelleImportsKey, combinedDeps) } if t.testonly { r.SetAttr("testonly", true) } r.SetPrivateAttr(resolvedDepsKey, t.resolvedDeps) + r.SetPrivateAttr(resolvedPyiDepsKey, t.resolvedPyiDeps) return r } + +// Combine both regular and type-checking imports into a single set +// for passing to the resolver. The resolver will distinguish them +// based on the TypeCheckingOnly field. +func (t *targetBuilder) combinedDeps() *treeset.Set { + combinedDeps := treeset.NewWith(moduleComparator) + combinedDeps.Add(t.pyiDeps.Values()...) + combinedDeps.Add(t.deps.Values()...) + return combinedDeps +} diff --git a/gazelle/python/testdata/add_type_stub_packages/BUILD.out b/gazelle/python/testdata/add_type_stub_packages/BUILD.out index d30540f61a..ae4e54c656 100644 --- a/gazelle/python/testdata/add_type_stub_packages/BUILD.out +++ b/gazelle/python/testdata/add_type_stub_packages/BUILD.out @@ -4,11 +4,13 @@ py_binary( name = "add_type_stub_packages_bin", srcs = ["__main__.py"], main = "__main__.py", + pyi_deps = [ + "@gazelle_python_test//boto3_stubs", + "@gazelle_python_test//django_types", + ], visibility = ["//:__subpackages__"], deps = [ "@gazelle_python_test//boto3", - "@gazelle_python_test//boto3_stubs", "@gazelle_python_test//django", - "@gazelle_python_test//django_types", ], ) diff --git a/gazelle/python/testdata/type_checking_imports/BUILD.in b/gazelle/python/testdata/type_checking_imports/BUILD.in new file mode 100644 index 0000000000..af2c2cea4b --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_generation_mode file diff --git a/gazelle/python/testdata/type_checking_imports/BUILD.out b/gazelle/python/testdata/type_checking_imports/BUILD.out new file mode 100644 index 0000000000..9b2dbbcae6 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/BUILD.out @@ -0,0 +1,25 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +py_library( + name = "bar", + srcs = ["bar.py"], + pyi_deps = [":foo"], + visibility = ["//:__subpackages__"], + deps = [":baz"], +) + +py_library( + name = "baz", + srcs = ["baz.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + pyi_deps = ["@gazelle_python_test//djangorestframework"], + visibility = ["//:__subpackages__"], + deps = ["@gazelle_python_test//boto3"], +) diff --git a/gazelle/python/testdata/type_checking_imports/README.md b/gazelle/python/testdata/type_checking_imports/README.md new file mode 100644 index 0000000000..b09f442be3 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/README.md @@ -0,0 +1,5 @@ +# Type Checking Imports + +Test that the Python gazelle correctly handles type-only imports inside `if TYPE_CHECKING:` blocks. + +Type-only imports should be added to the `pyi_deps` attribute instead of the regular `deps` attribute. diff --git a/gazelle/python/testdata/type_checking_imports/WORKSPACE b/gazelle/python/testdata/type_checking_imports/WORKSPACE new file mode 100644 index 0000000000..3e6e74e7f4 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "gazelle_python_test") diff --git a/gazelle/python/testdata/type_checking_imports/bar.py b/gazelle/python/testdata/type_checking_imports/bar.py new file mode 100644 index 0000000000..47c7d93d08 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/bar.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +# foo should be added as a pyi_deps, since it is only imported in a type-checking context, but baz should be +# added as a deps. +from baz import X + +if TYPE_CHECKING: + import baz + import foo diff --git a/gazelle/python/testdata/type_checking_imports/baz.py b/gazelle/python/testdata/type_checking_imports/baz.py new file mode 100644 index 0000000000..b2e9aa9265 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/baz.py @@ -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. + +X = 1 diff --git a/gazelle/python/testdata/type_checking_imports/foo.py b/gazelle/python/testdata/type_checking_imports/foo.py new file mode 100644 index 0000000000..99f58343d9 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/foo.py @@ -0,0 +1,20 @@ +# 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. + +import typing + +import boto3 + +if typing.TYPE_CHECKING: + from rest_framework import serializers diff --git a/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml new file mode 100644 index 0000000000..9ad07ec139 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml @@ -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. + +manifest: + modules_mapping: + boto3: boto3 + rest_framework: djangorestframework + pip_deps_repository_name: gazelle_python_test diff --git a/gazelle/python/testdata/type_checking_imports/test.yaml b/gazelle/python/testdata/type_checking_imports/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/type_checking_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. + +--- From 38dcd596b93938c66b5470beb97ca42c92250ec6 Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Sat, 21 Jun 2025 22:22:23 -0700 Subject: [PATCH 2/9] Add directive gazelle:python_generate_pyi_deps --- CHANGELOG.md | 7 +++-- gazelle/README.md | 2 ++ gazelle/python/configure.go | 7 +++++ gazelle/python/resolve.go | 17 ++++++++--- .../testdata/add_type_stub_packages/BUILD.in | 1 + .../testdata/add_type_stub_packages/BUILD.out | 2 ++ .../testdata/type_checking_imports/BUILD.in | 1 + .../testdata/type_checking_imports/BUILD.out | 1 + .../type_checking_imports_disabled/BUILD.in | 2 ++ .../type_checking_imports_disabled/BUILD.out | 30 +++++++++++++++++++ .../type_checking_imports_disabled/README.md | 3 ++ .../type_checking_imports_disabled/WORKSPACE | 1 + .../type_checking_imports_disabled/bar.py | 9 ++++++ .../type_checking_imports_disabled/baz.py | 15 ++++++++++ .../type_checking_imports_disabled/foo.py | 20 +++++++++++++ .../gazelle_python.yaml | 19 ++++++++++++ .../type_checking_imports_disabled/test.yaml | 15 ++++++++++ gazelle/pythonconfig/pythonconfig.go | 19 ++++++++++++ 18 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/BUILD.in create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/BUILD.out create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/README.md create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/WORKSPACE create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/bar.py create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/baz.py create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/foo.py create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml create mode 100644 gazelle/python/testdata/type_checking_imports_disabled/test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 22d3a76b07..b43528b4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,8 +55,11 @@ END_UNRELEASED_TEMPLATE {#v0-0-0-changed} ### Changed * (gazelle) Types for exposed members of `python.ParserOutput` are now all public. -* (gazelle) Dependencies added to satisfy type-only imports (`if TYPE_CHECKING`) and type stub packages are - now added to `pyi_deps` instead of `deps`. + +### Added +* (gazelle) New directive `gazelle:python_generate_pyi_deps`; when `true`, + dependencies added to satisfy type-only imports (`if TYPE_CHECKING`) and type + stub packages are added to `pyi_deps` instead of `deps`. {#v0-0-0-fixed} ### Fixed diff --git a/gazelle/README.md b/gazelle/README.md index 89ebaef4cd..2b5fd0ae10 100644 --- a/gazelle/README.md +++ b/gazelle/README.md @@ -220,6 +220,8 @@ Python-specific directives are as follows: | 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`. | | `# gazelle:python_label_normalization` | `snake_case` | | 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". | +| `# gazelle:python_generate_pyi_deps` | `false` | +| 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. | #### Directive: `python_root`: diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go index a00b0ba0ba..d52cecd2f1 100644 --- a/gazelle/python/configure.go +++ b/gazelle/python/configure.go @@ -68,6 +68,7 @@ func (py *Configurer) KnownDirectives() []string { pythonconfig.TestFilePattern, pythonconfig.LabelConvention, pythonconfig.LabelNormalization, + pythonconfig.GeneratePyiDeps, } } @@ -222,6 +223,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { default: config.SetLabelNormalization(pythonconfig.DefaultLabelNormalizationType) } + case pythonconfig.GeneratePyiDeps: + v, err := strconv.ParseBool(strings.TrimSpace(d.Value)) + if err != nil { + log.Fatal(err) + } + config.SetGeneratePyiDeps(v) } } diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index a4be080b56..fec46d1b30 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -155,10 +155,10 @@ func (py *Resolver) Resolve( // other generators that generate py_* targets. deps := treeset.NewWith(godsutils.StringComparator) pyiDeps := treeset.NewWith(godsutils.StringComparator) + cfgs := c.Exts[languageName].(pythonconfig.Configs) + cfg := cfgs[from.Pkg] if modulesRaw != nil { - cfgs := c.Exts[languageName].(pythonconfig.Configs) - cfg := cfgs[from.Pkg] pythonProjectRoot := cfg.PythonProjectRoot() modules := modulesRaw.(*treeset.Set) it := modules.Iterator() @@ -301,8 +301,17 @@ func (py *Resolver) Resolve( } } - addResolvedDepsAndSetAttr(r, deps, resolvedDepsKey, "deps") - addResolvedDepsAndSetAttr(r, pyiDeps, resolvedPyiDepsKey, "pyi_deps") + if cfg.GeneratePyiDeps() { + addResolvedDepsAndSetAttr(r, deps, resolvedDepsKey, "deps") + addResolvedDepsAndSetAttr(r, pyiDeps, resolvedPyiDepsKey, "pyi_deps") + } else { + // When generate_pyi_deps is false, merge both deps and pyiDeps into deps + combinedDeps := treeset.NewWith(godsutils.StringComparator) + combinedDeps.Add(deps.Values()...) + combinedDeps.Add(pyiDeps.Values()...) + + addResolvedDepsAndSetAttr(r, combinedDeps, resolvedDepsKey, "deps") + } } // addResolvedDepsAndSetAttr adds the pre-resolved dependencies from the rule's private attributes diff --git a/gazelle/python/testdata/add_type_stub_packages/BUILD.in b/gazelle/python/testdata/add_type_stub_packages/BUILD.in index e69de29bb2..99d122ad12 100644 --- a/gazelle/python/testdata/add_type_stub_packages/BUILD.in +++ b/gazelle/python/testdata/add_type_stub_packages/BUILD.in @@ -0,0 +1 @@ +# gazelle:python_generate_pyi_deps true diff --git a/gazelle/python/testdata/add_type_stub_packages/BUILD.out b/gazelle/python/testdata/add_type_stub_packages/BUILD.out index ae4e54c656..1a5b640ac8 100644 --- a/gazelle/python/testdata/add_type_stub_packages/BUILD.out +++ b/gazelle/python/testdata/add_type_stub_packages/BUILD.out @@ -1,5 +1,7 @@ load("@rules_python//python:defs.bzl", "py_binary") +# gazelle:python_generate_pyi_deps true + py_binary( name = "add_type_stub_packages_bin", srcs = ["__main__.py"], diff --git a/gazelle/python/testdata/type_checking_imports/BUILD.in b/gazelle/python/testdata/type_checking_imports/BUILD.in index af2c2cea4b..d4dce063ef 100644 --- a/gazelle/python/testdata/type_checking_imports/BUILD.in +++ b/gazelle/python/testdata/type_checking_imports/BUILD.in @@ -1 +1,2 @@ # gazelle:python_generation_mode file +# gazelle:python_generate_pyi_deps true diff --git a/gazelle/python/testdata/type_checking_imports/BUILD.out b/gazelle/python/testdata/type_checking_imports/BUILD.out index 9b2dbbcae6..da9340194e 100644 --- a/gazelle/python/testdata/type_checking_imports/BUILD.out +++ b/gazelle/python/testdata/type_checking_imports/BUILD.out @@ -1,6 +1,7 @@ load("@rules_python//python:defs.bzl", "py_library") # gazelle:python_generation_mode file +# gazelle:python_generate_pyi_deps true py_library( name = "bar", diff --git a/gazelle/python/testdata/type_checking_imports_disabled/BUILD.in b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.in new file mode 100644 index 0000000000..ab6d30f5a7 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode file +# gazelle:python_generate_pyi_deps false diff --git a/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out new file mode 100644 index 0000000000..fcc4c23443 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out @@ -0,0 +1,30 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file +# gazelle:python_generate_pyi_deps false + +py_library( + name = "bar", + srcs = ["bar.py"], + visibility = ["//:__subpackages__"], + deps = [ + ":baz", + ":foo", + ], +) + +py_library( + name = "baz", + srcs = ["baz.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], + deps = [ + "@gazelle_python_test//boto3", + "@gazelle_python_test//djangorestframework", + ], +) diff --git a/gazelle/python/testdata/type_checking_imports_disabled/README.md b/gazelle/python/testdata/type_checking_imports_disabled/README.md new file mode 100644 index 0000000000..0e3b623614 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/README.md @@ -0,0 +1,3 @@ +# Type Checking Imports (disabled) + +See `type_checking_imports`; this is the same test case, but with the directive disabled. diff --git a/gazelle/python/testdata/type_checking_imports_disabled/WORKSPACE b/gazelle/python/testdata/type_checking_imports_disabled/WORKSPACE new file mode 100644 index 0000000000..3e6e74e7f4 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "gazelle_python_test") diff --git a/gazelle/python/testdata/type_checking_imports_disabled/bar.py b/gazelle/python/testdata/type_checking_imports_disabled/bar.py new file mode 100644 index 0000000000..47c7d93d08 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/bar.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +# foo should be added as a pyi_deps, since it is only imported in a type-checking context, but baz should be +# added as a deps. +from baz import X + +if TYPE_CHECKING: + import baz + import foo diff --git a/gazelle/python/testdata/type_checking_imports_disabled/baz.py b/gazelle/python/testdata/type_checking_imports_disabled/baz.py new file mode 100644 index 0000000000..b2e9aa9265 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/baz.py @@ -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. + +X = 1 diff --git a/gazelle/python/testdata/type_checking_imports_disabled/foo.py b/gazelle/python/testdata/type_checking_imports_disabled/foo.py new file mode 100644 index 0000000000..99f58343d9 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/foo.py @@ -0,0 +1,20 @@ +# 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. + +import typing + +import boto3 + +if typing.TYPE_CHECKING: + from rest_framework import serializers diff --git a/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml new file mode 100644 index 0000000000..9ad07ec139 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml @@ -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. + +manifest: + modules_mapping: + boto3: boto3 + rest_framework: djangorestframework + pip_deps_repository_name: gazelle_python_test diff --git a/gazelle/python/testdata/type_checking_imports_disabled/test.yaml b/gazelle/python/testdata/type_checking_imports_disabled/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_disabled/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. + +--- diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index 866339d449..ff8898522b 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -91,6 +91,10 @@ const ( // names of labels to third-party dependencies are normalized. Supported values // are 'none', 'pep503' and 'snake_case' (default). See LabelNormalizationType. LabelNormalization = "python_label_normalization" + // GeneratePyiDeps represents the directive that controls whether to generate + // separate pyi_deps attribute or merge type-checking dependencies into deps. + // Defaults to false for backward compatibility. + GeneratePyiDeps = "python_generate_pyi_deps" ) // GenerationModeType represents one of the generation modes for the Python @@ -177,6 +181,7 @@ type Config struct { testFilePattern []string labelConvention string labelNormalization LabelNormalizationType + generatePyiDeps bool } type LabelNormalizationType int @@ -212,6 +217,7 @@ func New( testFilePattern: strings.Split(DefaultTestFilePatternString, ","), labelConvention: DefaultLabelConvention, labelNormalization: DefaultLabelNormalizationType, + generatePyiDeps: false, } } @@ -244,6 +250,7 @@ func (c *Config) NewChild() *Config { testFilePattern: c.testFilePattern, labelConvention: c.labelConvention, labelNormalization: c.labelNormalization, + generatePyiDeps: c.generatePyiDeps, } } @@ -520,6 +527,18 @@ func (c *Config) LabelNormalization() LabelNormalizationType { return c.labelNormalization } +// SetGeneratePyiDeps sets whether pyi_deps attribute should be generated separately +// or type-checking dependencies should be merged into the regular deps attribute. +func (c *Config) SetGeneratePyiDeps(generatePyiDeps bool) { + c.generatePyiDeps = generatePyiDeps +} + +// GeneratePyiDeps returns whether pyi_deps attribute should be generated separately +// or type-checking dependencies should be merged into the regular deps attribute. +func (c *Config) GeneratePyiDeps() bool { + return c.generatePyiDeps +} + // 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) From 5479ea6952c7ecd0e579eda2ff110a0e226532fa Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 00:37:07 -0700 Subject: [PATCH 3/9] Revert changes to target.go and simplify resolve.go --- gazelle/python/resolve.go | 28 +++++++++++++++------------- gazelle/python/target.go | 30 ++++-------------------------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index fec46d1b30..3a660abf6d 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -301,38 +301,40 @@ func (py *Resolver) Resolve( } } + addResolvedDeps(r, deps) + if cfg.GeneratePyiDeps() { - addResolvedDepsAndSetAttr(r, deps, resolvedDepsKey, "deps") - addResolvedDepsAndSetAttr(r, pyiDeps, resolvedPyiDepsKey, "pyi_deps") + if !deps.Empty() { + r.SetAttr("deps", convertDependencySetToExpr(deps)) + } + if !pyiDeps.Empty() { + r.SetAttr("pyi_deps", convertDependencySetToExpr(pyiDeps)) + } } else { // When generate_pyi_deps is false, merge both deps and pyiDeps into deps combinedDeps := treeset.NewWith(godsutils.StringComparator) combinedDeps.Add(deps.Values()...) combinedDeps.Add(pyiDeps.Values()...) - addResolvedDepsAndSetAttr(r, combinedDeps, resolvedDepsKey, "deps") + if !combinedDeps.Empty() { + r.SetAttr("deps", convertDependencySetToExpr(combinedDeps)) + } } } -// addResolvedDepsAndSetAttr adds the pre-resolved dependencies from the rule's private attributes -// to the provided deps set and sets the attribute on the rule. -func addResolvedDepsAndSetAttr( +// addResolvedDeps adds the pre-resolved dependencies from the rule's private attributes +// to the provided deps set. +func addResolvedDeps( r *rule.Rule, deps *treeset.Set, - resolvedDepsAttrName string, - depsAttrName string, ) { - resolvedDeps := r.PrivateAttr(resolvedDepsAttrName).(*treeset.Set) + resolvedDeps := r.PrivateAttr(resolvedDepsKey).(*treeset.Set) if !resolvedDeps.Empty() { it := resolvedDeps.Iterator() for it.Next() { deps.Add(it.Value()) } } - - if !deps.Empty() { - r.SetAttr(depsAttrName, convertDependencySetToExpr(deps)) - } } // targetListFromResults returns a string with the human-readable list of diff --git a/gazelle/python/target.go b/gazelle/python/target.go index 326ac2172a..1fb9218656 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -15,12 +15,11 @@ package python import ( - "path/filepath" - "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/rule" "github.com/emirpasic/gods/sets/treeset" godsutils "github.com/emirpasic/gods/utils" + "path/filepath" ) // targetBuilder builds targets to be generated by Gazelle. @@ -32,9 +31,7 @@ type targetBuilder struct { srcs *treeset.Set siblingSrcs *treeset.Set deps *treeset.Set - pyiDeps *treeset.Set resolvedDeps *treeset.Set - resolvedPyiDeps *treeset.Set visibility *treeset.Set main *string imports []string @@ -51,9 +48,7 @@ func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingS srcs: treeset.NewWith(godsutils.StringComparator), siblingSrcs: siblingSrcs, deps: treeset.NewWith(moduleComparator), - pyiDeps: treeset.NewWith(moduleComparator), resolvedDeps: treeset.NewWith(godsutils.StringComparator), - resolvedPyiDeps: treeset.NewWith(godsutils.StringComparator), visibility: treeset.NewWith(godsutils.StringComparator), } } @@ -84,13 +79,7 @@ func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { // dependency resolution easier dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp } - - // Add to appropriate dependency set based on whether it's type-checking only - if dep.TypeCheckingOnly { - t.pyiDeps.Add(dep) - } else { - t.deps.Add(dep) - } + t.deps.Add(dep) return t } @@ -173,23 +162,12 @@ func (t *targetBuilder) build() *rule.Rule { if t.imports != nil { r.SetAttr("imports", t.imports) } - if combinedDeps := t.combinedDeps(); !combinedDeps.Empty() { - r.SetPrivateAttr(config.GazelleImportsKey, combinedDeps) + if !t.deps.Empty() { + r.SetPrivateAttr(config.GazelleImportsKey, t.deps) } if t.testonly { r.SetAttr("testonly", true) } r.SetPrivateAttr(resolvedDepsKey, t.resolvedDeps) - r.SetPrivateAttr(resolvedPyiDepsKey, t.resolvedPyiDeps) return r } - -// Combine both regular and type-checking imports into a single set -// for passing to the resolver. The resolver will distinguish them -// based on the TypeCheckingOnly field. -func (t *targetBuilder) combinedDeps() *treeset.Set { - combinedDeps := treeset.NewWith(moduleComparator) - combinedDeps.Add(t.pyiDeps.Values()...) - combinedDeps.Add(t.deps.Values()...) - return combinedDeps -} From 8208696f85a8c38cddafebfed581538a267617f4 Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 16:20:47 -0700 Subject: [PATCH 4/9] Adjust README for add_type_stub_packages --- gazelle/python/testdata/add_type_stub_packages/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gazelle/python/testdata/add_type_stub_packages/README.md b/gazelle/python/testdata/add_type_stub_packages/README.md index c42e76f8be..e3a2afee81 100644 --- a/gazelle/python/testdata/add_type_stub_packages/README.md +++ b/gazelle/python/testdata/add_type_stub_packages/README.md @@ -1,4 +1,4 @@ # Add stubs to `deps` of `py_library` target -This test case asserts that -* if a package has the corresponding stub available, it is added to the `deps` of the `py_library` target. +This test case asserts that +* if a package has the corresponding stub available, it is added to the `pyi_deps` of the `py_library` target. From 9f3f1c81221c9ace32b867cb4fef913dacec335b Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 16:57:09 -0700 Subject: [PATCH 5/9] Include boto3-stubs in gazelle_python.yaml on type_checking_imports test --- gazelle/python/testdata/type_checking_imports/BUILD.out | 9 ++++++++- gazelle/python/testdata/type_checking_imports/baz.py | 8 ++++++++ gazelle/python/testdata/type_checking_imports/foo.py | 1 + .../testdata/type_checking_imports/gazelle_python.yaml | 1 + .../testdata/type_checking_imports_disabled/BUILD.out | 5 +++++ .../testdata/type_checking_imports_disabled/baz.py | 8 ++++++++ .../testdata/type_checking_imports_disabled/foo.py | 1 + .../type_checking_imports_disabled/gazelle_python.yaml | 1 + 8 files changed, 33 insertions(+), 1 deletion(-) diff --git a/gazelle/python/testdata/type_checking_imports/BUILD.out b/gazelle/python/testdata/type_checking_imports/BUILD.out index da9340194e..690210682c 100644 --- a/gazelle/python/testdata/type_checking_imports/BUILD.out +++ b/gazelle/python/testdata/type_checking_imports/BUILD.out @@ -14,13 +14,20 @@ py_library( py_library( name = "baz", srcs = ["baz.py"], + pyi_deps = [ + "@gazelle_python_test//boto3", + "@gazelle_python_test//boto3_stubs", + ], visibility = ["//:__subpackages__"], ) py_library( name = "foo", srcs = ["foo.py"], - pyi_deps = ["@gazelle_python_test//djangorestframework"], + pyi_deps = [ + "@gazelle_python_test//boto3_stubs", + "@gazelle_python_test//djangorestframework", + ], visibility = ["//:__subpackages__"], deps = ["@gazelle_python_test//boto3"], ) diff --git a/gazelle/python/testdata/type_checking_imports/baz.py b/gazelle/python/testdata/type_checking_imports/baz.py index b2e9aa9265..1c69e25da4 100644 --- a/gazelle/python/testdata/type_checking_imports/baz.py +++ b/gazelle/python/testdata/type_checking_imports/baz.py @@ -12,4 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. + +# While this format is not official, it is supported by most type checkers and +# is used in the wild to avoid importing the typing module. +TYPE_CHECKING = False +if TYPE_CHECKING: + # Both boto3 and boto3_stubs should be added to pyi_deps. + import boto3 + X = 1 diff --git a/gazelle/python/testdata/type_checking_imports/foo.py b/gazelle/python/testdata/type_checking_imports/foo.py index 99f58343d9..655cb54675 100644 --- a/gazelle/python/testdata/type_checking_imports/foo.py +++ b/gazelle/python/testdata/type_checking_imports/foo.py @@ -14,6 +14,7 @@ import typing +# boto3 should be added to deps. boto3_stubs and djangorestframework should be added to pyi_deps. import boto3 if typing.TYPE_CHECKING: diff --git a/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml index 9ad07ec139..a782354215 100644 --- a/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml +++ b/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml @@ -15,5 +15,6 @@ manifest: modules_mapping: boto3: boto3 + boto3_stubs: boto3_stubs rest_framework: djangorestframework pip_deps_repository_name: gazelle_python_test diff --git a/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out index fcc4c23443..bf23d28da9 100644 --- a/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out +++ b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out @@ -17,6 +17,10 @@ py_library( name = "baz", srcs = ["baz.py"], visibility = ["//:__subpackages__"], + deps = [ + "@gazelle_python_test//boto3", + "@gazelle_python_test//boto3_stubs", + ], ) py_library( @@ -25,6 +29,7 @@ py_library( visibility = ["//:__subpackages__"], deps = [ "@gazelle_python_test//boto3", + "@gazelle_python_test//boto3_stubs", "@gazelle_python_test//djangorestframework", ], ) diff --git a/gazelle/python/testdata/type_checking_imports_disabled/baz.py b/gazelle/python/testdata/type_checking_imports_disabled/baz.py index b2e9aa9265..1c69e25da4 100644 --- a/gazelle/python/testdata/type_checking_imports_disabled/baz.py +++ b/gazelle/python/testdata/type_checking_imports_disabled/baz.py @@ -12,4 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. + +# While this format is not official, it is supported by most type checkers and +# is used in the wild to avoid importing the typing module. +TYPE_CHECKING = False +if TYPE_CHECKING: + # Both boto3 and boto3_stubs should be added to pyi_deps. + import boto3 + X = 1 diff --git a/gazelle/python/testdata/type_checking_imports_disabled/foo.py b/gazelle/python/testdata/type_checking_imports_disabled/foo.py index 99f58343d9..655cb54675 100644 --- a/gazelle/python/testdata/type_checking_imports_disabled/foo.py +++ b/gazelle/python/testdata/type_checking_imports_disabled/foo.py @@ -14,6 +14,7 @@ import typing +# boto3 should be added to deps. boto3_stubs and djangorestframework should be added to pyi_deps. import boto3 if typing.TYPE_CHECKING: diff --git a/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml index 9ad07ec139..a782354215 100644 --- a/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml +++ b/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml @@ -15,5 +15,6 @@ manifest: modules_mapping: boto3: boto3 + boto3_stubs: boto3_stubs rest_framework: djangorestframework pip_deps_repository_name: gazelle_python_test From b528dd8d40962d3a62b02f3372d88fb14e329b49 Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 17:36:57 -0700 Subject: [PATCH 6/9] Add tests for project / package mode; make the exported treesets deterministic. --- gazelle/python/parser.go | 13 +++++++-- gazelle/python/target.go | 29 ++++++++++++++++--- .../type_checking_imports_package/BUILD.in | 2 ++ .../type_checking_imports_package/BUILD.out | 19 ++++++++++++ .../type_checking_imports_package/README.md | 3 ++ .../type_checking_imports_package/WORKSPACE | 1 + .../type_checking_imports_package/bar.py | 9 ++++++ .../type_checking_imports_package/baz.py | 23 +++++++++++++++ .../type_checking_imports_package/foo.py | 21 ++++++++++++++ .../gazelle_python.yaml | 20 +++++++++++++ .../type_checking_imports_package/test.yaml | 15 ++++++++++ .../type_checking_imports_project/BUILD.in | 2 ++ .../type_checking_imports_project/BUILD.out | 19 ++++++++++++ .../type_checking_imports_project/README.md | 3 ++ .../type_checking_imports_project/WORKSPACE | 1 + .../type_checking_imports_project/bar.py | 9 ++++++ .../type_checking_imports_project/baz.py | 23 +++++++++++++++ .../type_checking_imports_project/foo.py | 21 ++++++++++++++ .../gazelle_python.yaml | 20 +++++++++++++ .../type_checking_imports_project/test.yaml | 15 ++++++++++ 20 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 gazelle/python/testdata/type_checking_imports_package/BUILD.in create mode 100644 gazelle/python/testdata/type_checking_imports_package/BUILD.out create mode 100644 gazelle/python/testdata/type_checking_imports_package/README.md create mode 100644 gazelle/python/testdata/type_checking_imports_package/WORKSPACE create mode 100644 gazelle/python/testdata/type_checking_imports_package/bar.py create mode 100644 gazelle/python/testdata/type_checking_imports_package/baz.py create mode 100644 gazelle/python/testdata/type_checking_imports_package/foo.py create mode 100644 gazelle/python/testdata/type_checking_imports_package/gazelle_python.yaml create mode 100644 gazelle/python/testdata/type_checking_imports_package/test.yaml create mode 100644 gazelle/python/testdata/type_checking_imports_project/BUILD.in create mode 100644 gazelle/python/testdata/type_checking_imports_project/BUILD.out create mode 100644 gazelle/python/testdata/type_checking_imports_project/README.md create mode 100644 gazelle/python/testdata/type_checking_imports_project/WORKSPACE create mode 100644 gazelle/python/testdata/type_checking_imports_project/bar.py create mode 100644 gazelle/python/testdata/type_checking_imports_project/baz.py create mode 100644 gazelle/python/testdata/type_checking_imports_project/foo.py create mode 100644 gazelle/python/testdata/type_checking_imports_project/gazelle_python.yaml create mode 100644 gazelle/python/testdata/type_checking_imports_project/test.yaml diff --git a/gazelle/python/parser.go b/gazelle/python/parser.go index 163a806619..11e01dbf51 100644 --- a/gazelle/python/parser.go +++ b/gazelle/python/parser.go @@ -112,9 +112,9 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, map[strin continue } - modules.Add(m) + addModuleToTreeSet(modules, m) if res.HasMain { - mainModules[res.FileName].Add(m) + addModuleToTreeSet(mainModules[res.FileName], m) } } @@ -167,6 +167,15 @@ func moduleComparator(a, b interface{}) int { return godsutils.StringComparator(a.(Module).Name, b.(Module).Name) } +// addModuleToTreeSet adds a module to a treeset.Set, ensuring that a TypeCheckingOnly=false module is +// prefered over a TypeCheckingOnly=true module. +func addModuleToTreeSet(set *treeset.Set, mod Module) { + if mod.TypeCheckingOnly && set.Contains(mod) { + return + } + set.Add(mod) +} + // annotationKind represents Gazelle annotation kinds. type annotationKind string diff --git a/gazelle/python/target.go b/gazelle/python/target.go index 1fb9218656..5a92448bc8 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -15,11 +15,12 @@ package python import ( + "path/filepath" + "github.com/bazelbuild/bazel-gazelle/config" "github.com/bazelbuild/bazel-gazelle/rule" "github.com/emirpasic/gods/sets/treeset" godsutils "github.com/emirpasic/gods/utils" - "path/filepath" ) // targetBuilder builds targets to be generated by Gazelle. @@ -31,6 +32,7 @@ type targetBuilder struct { srcs *treeset.Set siblingSrcs *treeset.Set deps *treeset.Set + pyiDeps *treeset.Set resolvedDeps *treeset.Set visibility *treeset.Set main *string @@ -48,6 +50,7 @@ func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingS srcs: treeset.NewWith(godsutils.StringComparator), siblingSrcs: siblingSrcs, deps: treeset.NewWith(moduleComparator), + pyiDeps: treeset.NewWith(moduleComparator), resolvedDeps: treeset.NewWith(godsutils.StringComparator), visibility: treeset.NewWith(godsutils.StringComparator), } @@ -79,7 +82,13 @@ func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { // dependency resolution easier dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp } - t.deps.Add(dep) + + // Add to appropriate dependency set based on whether it's type-checking only + if dep.TypeCheckingOnly { + t.pyiDeps.Add(dep) + } else { + t.deps.Add(dep) + } return t } @@ -162,8 +171,8 @@ func (t *targetBuilder) build() *rule.Rule { if t.imports != nil { r.SetAttr("imports", t.imports) } - if !t.deps.Empty() { - r.SetPrivateAttr(config.GazelleImportsKey, t.deps) + if combinedDeps := t.combinedDeps(); !combinedDeps.Empty() { + r.SetPrivateAttr(config.GazelleImportsKey, combinedDeps) } if t.testonly { r.SetAttr("testonly", true) @@ -171,3 +180,15 @@ func (t *targetBuilder) build() *rule.Rule { r.SetPrivateAttr(resolvedDepsKey, t.resolvedDeps) return r } + +// Combine both regular and type-checking imports into a single set +// for passing to the resolver. The resolver will distinguish them +// based on the TypeCheckingOnly field. +func (t *targetBuilder) combinedDeps() *treeset.Set { + combinedDeps := treeset.NewWith(moduleComparator) + // If an import is in both pyi_deps and deps, the one in deps will override the one in pyi_deps, resulting + // in the resolver properly adding to deps instead of pyi_deps. + combinedDeps.Add(t.pyiDeps.Values()...) + combinedDeps.Add(t.deps.Values()...) + return combinedDeps +} diff --git a/gazelle/python/testdata/type_checking_imports_package/BUILD.in b/gazelle/python/testdata/type_checking_imports_package/BUILD.in new file mode 100644 index 0000000000..8e6c1cbabb --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode package +# gazelle:python_generate_pyi_deps true diff --git a/gazelle/python/testdata/type_checking_imports_package/BUILD.out b/gazelle/python/testdata/type_checking_imports_package/BUILD.out new file mode 100644 index 0000000000..0091e9c5c9 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/BUILD.out @@ -0,0 +1,19 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode package +# gazelle:python_generate_pyi_deps true + +py_library( + name = "type_checking_imports_package", + srcs = [ + "bar.py", + "baz.py", + "foo.py", + ], + pyi_deps = [ + "@gazelle_python_test//boto3_stubs", + "@gazelle_python_test//djangorestframework", + ], + visibility = ["//:__subpackages__"], + deps = ["@gazelle_python_test//boto3"], +) diff --git a/gazelle/python/testdata/type_checking_imports_package/README.md b/gazelle/python/testdata/type_checking_imports_package/README.md new file mode 100644 index 0000000000..3e2cafe992 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/README.md @@ -0,0 +1,3 @@ +# Type Checking Imports (package mode) + +See `type_checking_imports`; this is the same test case, but using the package generation mode. diff --git a/gazelle/python/testdata/type_checking_imports_package/WORKSPACE b/gazelle/python/testdata/type_checking_imports_package/WORKSPACE new file mode 100644 index 0000000000..3e6e74e7f4 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "gazelle_python_test") diff --git a/gazelle/python/testdata/type_checking_imports_package/bar.py b/gazelle/python/testdata/type_checking_imports_package/bar.py new file mode 100644 index 0000000000..47c7d93d08 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/bar.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +# foo should be added as a pyi_deps, since it is only imported in a type-checking context, but baz should be +# added as a deps. +from baz import X + +if TYPE_CHECKING: + import baz + import foo diff --git a/gazelle/python/testdata/type_checking_imports_package/baz.py b/gazelle/python/testdata/type_checking_imports_package/baz.py new file mode 100644 index 0000000000..1c69e25da4 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/baz.py @@ -0,0 +1,23 @@ +# 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. + + +# While this format is not official, it is supported by most type checkers and +# is used in the wild to avoid importing the typing module. +TYPE_CHECKING = False +if TYPE_CHECKING: + # Both boto3 and boto3_stubs should be added to pyi_deps. + import boto3 + +X = 1 diff --git a/gazelle/python/testdata/type_checking_imports_package/foo.py b/gazelle/python/testdata/type_checking_imports_package/foo.py new file mode 100644 index 0000000000..655cb54675 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/foo.py @@ -0,0 +1,21 @@ +# 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. + +import typing + +# boto3 should be added to deps. boto3_stubs and djangorestframework should be added to pyi_deps. +import boto3 + +if typing.TYPE_CHECKING: + from rest_framework import serializers diff --git a/gazelle/python/testdata/type_checking_imports_package/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports_package/gazelle_python.yaml new file mode 100644 index 0000000000..a782354215 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/gazelle_python.yaml @@ -0,0 +1,20 @@ +# 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. + +manifest: + modules_mapping: + boto3: boto3 + boto3_stubs: boto3_stubs + rest_framework: djangorestframework + pip_deps_repository_name: gazelle_python_test diff --git a/gazelle/python/testdata/type_checking_imports_package/test.yaml b/gazelle/python/testdata/type_checking_imports_package/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_package/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. + +--- diff --git a/gazelle/python/testdata/type_checking_imports_project/BUILD.in b/gazelle/python/testdata/type_checking_imports_project/BUILD.in new file mode 100644 index 0000000000..808e3e044e --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/BUILD.in @@ -0,0 +1,2 @@ +# gazelle:python_generation_mode project +# gazelle:python_generate_pyi_deps true diff --git a/gazelle/python/testdata/type_checking_imports_project/BUILD.out b/gazelle/python/testdata/type_checking_imports_project/BUILD.out new file mode 100644 index 0000000000..6d6ac3cef9 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/BUILD.out @@ -0,0 +1,19 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode project +# gazelle:python_generate_pyi_deps true + +py_library( + name = "type_checking_imports_project", + srcs = [ + "bar.py", + "baz.py", + "foo.py", + ], + pyi_deps = [ + "@gazelle_python_test//boto3_stubs", + "@gazelle_python_test//djangorestframework", + ], + visibility = ["//:__subpackages__"], + deps = ["@gazelle_python_test//boto3"], +) diff --git a/gazelle/python/testdata/type_checking_imports_project/README.md b/gazelle/python/testdata/type_checking_imports_project/README.md new file mode 100644 index 0000000000..ead09e1994 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/README.md @@ -0,0 +1,3 @@ +# Type Checking Imports (project mode) + +See `type_checking_imports`; this is the same test case, but using the project generation mode. diff --git a/gazelle/python/testdata/type_checking_imports_project/WORKSPACE b/gazelle/python/testdata/type_checking_imports_project/WORKSPACE new file mode 100644 index 0000000000..3e6e74e7f4 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "gazelle_python_test") diff --git a/gazelle/python/testdata/type_checking_imports_project/bar.py b/gazelle/python/testdata/type_checking_imports_project/bar.py new file mode 100644 index 0000000000..47c7d93d08 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/bar.py @@ -0,0 +1,9 @@ +from typing import TYPE_CHECKING + +# foo should be added as a pyi_deps, since it is only imported in a type-checking context, but baz should be +# added as a deps. +from baz import X + +if TYPE_CHECKING: + import baz + import foo diff --git a/gazelle/python/testdata/type_checking_imports_project/baz.py b/gazelle/python/testdata/type_checking_imports_project/baz.py new file mode 100644 index 0000000000..1c69e25da4 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/baz.py @@ -0,0 +1,23 @@ +# 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. + + +# While this format is not official, it is supported by most type checkers and +# is used in the wild to avoid importing the typing module. +TYPE_CHECKING = False +if TYPE_CHECKING: + # Both boto3 and boto3_stubs should be added to pyi_deps. + import boto3 + +X = 1 diff --git a/gazelle/python/testdata/type_checking_imports_project/foo.py b/gazelle/python/testdata/type_checking_imports_project/foo.py new file mode 100644 index 0000000000..655cb54675 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/foo.py @@ -0,0 +1,21 @@ +# 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. + +import typing + +# boto3 should be added to deps. boto3_stubs and djangorestframework should be added to pyi_deps. +import boto3 + +if typing.TYPE_CHECKING: + from rest_framework import serializers diff --git a/gazelle/python/testdata/type_checking_imports_project/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports_project/gazelle_python.yaml new file mode 100644 index 0000000000..a782354215 --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/gazelle_python.yaml @@ -0,0 +1,20 @@ +# 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. + +manifest: + modules_mapping: + boto3: boto3 + boto3_stubs: boto3_stubs + rest_framework: djangorestframework + pip_deps_repository_name: gazelle_python_test diff --git a/gazelle/python/testdata/type_checking_imports_project/test.yaml b/gazelle/python/testdata/type_checking_imports_project/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/type_checking_imports_project/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. + +--- From cdf413c9cf857bce8f7e04c35a28b1e24cf364e7 Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 17:39:54 -0700 Subject: [PATCH 7/9] Use addModuleToTreeSet --- gazelle/python/target.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/gazelle/python/target.go b/gazelle/python/target.go index 5a92448bc8..06b653d915 100644 --- a/gazelle/python/target.go +++ b/gazelle/python/target.go @@ -32,7 +32,6 @@ type targetBuilder struct { srcs *treeset.Set siblingSrcs *treeset.Set deps *treeset.Set - pyiDeps *treeset.Set resolvedDeps *treeset.Set visibility *treeset.Set main *string @@ -50,7 +49,6 @@ func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingS srcs: treeset.NewWith(godsutils.StringComparator), siblingSrcs: siblingSrcs, deps: treeset.NewWith(moduleComparator), - pyiDeps: treeset.NewWith(moduleComparator), resolvedDeps: treeset.NewWith(godsutils.StringComparator), visibility: treeset.NewWith(godsutils.StringComparator), } @@ -83,12 +81,7 @@ func (t *targetBuilder) addModuleDependency(dep Module) *targetBuilder { dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp } - // Add to appropriate dependency set based on whether it's type-checking only - if dep.TypeCheckingOnly { - t.pyiDeps.Add(dep) - } else { - t.deps.Add(dep) - } + addModuleToTreeSet(t.deps, dep) return t } @@ -171,8 +164,8 @@ func (t *targetBuilder) build() *rule.Rule { if t.imports != nil { r.SetAttr("imports", t.imports) } - if combinedDeps := t.combinedDeps(); !combinedDeps.Empty() { - r.SetPrivateAttr(config.GazelleImportsKey, combinedDeps) + if !t.deps.Empty() { + r.SetPrivateAttr(config.GazelleImportsKey, t.deps) } if t.testonly { r.SetAttr("testonly", true) @@ -180,15 +173,3 @@ func (t *targetBuilder) build() *rule.Rule { r.SetPrivateAttr(resolvedDepsKey, t.resolvedDeps) return r } - -// Combine both regular and type-checking imports into a single set -// for passing to the resolver. The resolver will distinguish them -// based on the TypeCheckingOnly field. -func (t *targetBuilder) combinedDeps() *treeset.Set { - combinedDeps := treeset.NewWith(moduleComparator) - // If an import is in both pyi_deps and deps, the one in deps will override the one in pyi_deps, resulting - // in the resolver properly adding to deps instead of pyi_deps. - combinedDeps.Add(t.pyiDeps.Values()...) - combinedDeps.Add(t.deps.Values()...) - return combinedDeps -} From e99a53584bc07fbd093ca3a7abf796568b428059 Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 21:18:16 -0700 Subject: [PATCH 8/9] Merge added sections of the changelog --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4afac225e..78a3d1caf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,11 +59,6 @@ END_UNRELEASED_TEMPLATE `# gazelle:experimental_allow_relative_imports` true directive ({gh-issue}`2203`). * (gazelle) Types for exposed members of `python.ParserOutput` are now all public. -### Added -* (gazelle) New directive `gazelle:python_generate_pyi_deps`; when `true`, - dependencies added to satisfy type-only imports (`if TYPE_CHECKING`) and type - stub packages are added to `pyi_deps` instead of `deps`. - {#v0-0-0-fixed} ### Fixed * (pypi) Fixes an issue where builds using a `bazel vendor` vendor directory @@ -75,6 +70,9 @@ END_UNRELEASED_TEMPLATE * (pypi) To configure the environment for `requirements.txt` evaluation, use the newly added developer preview of the `pip.default` tag class. Only `rules_python` and root modules can use this feature. You can also configure custom `config_settings` using `pip.default`. +* (gazelle) New directive `gazelle:python_generate_pyi_deps`; when `true`, + dependencies added to satisfy type-only imports (`if TYPE_CHECKING`) and type + stub packages are added to `pyi_deps` instead of `deps`. {#v0-0-0-removed} ### Removed From 80d215f79eb9cd7b0dc654c6817ff4586bfcdb1e Mon Sep 17 00:00:00 2001 From: Alex Martani Date: Mon, 23 Jun 2025 21:43:37 -0700 Subject: [PATCH 9/9] Remove resolvedPyiDepsKey (unused) --- gazelle/python/resolve.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index d58284ec28..88275e007c 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -39,9 +39,6 @@ const ( // resolvedDepsKey is the attribute key used to pass dependencies that don't // need to be resolved by the dependency resolver in the Resolver step. resolvedDepsKey = "_gazelle_python_resolved_deps" - // resolvedPyiDepsKey is the attribute key used to pass type-checking dependencies that don't - // need to be resolved by the dependency resolver in the Resolver step. - resolvedPyiDepsKey = "_gazelle_python_resolved_pyi_deps" ) // Resolver satisfies the resolve.Resolver interface. It resolves dependencies