feat(gazelle): Add type-checking only dependencies to pyi_deps (#3014)
https://github.com/bazel-contrib/rules_python/pull/2538 added the
attribute `pyi_deps` to python rules, intended to be used for
dependencies that are only used for type-checking purposes. This PR adds
a new directive, `gazelle:python_generate_pyi_deps`, which, when
enabled:
- When a dependency is added only to satisfy type-checking only imports
(in a `if TYPE_CHECKING:` block), the dependency is added to `pyi_deps`
instead of `deps`;
- Third-party stub packages (eg. `boto3-stubs`) are now added to
`pyi_deps` instead of `deps`.
---------
Co-authored-by: Douglas Thor <dougthor42@users.noreply.github.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4facff4..78a3d1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -70,6 +70,9 @@
* (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
diff --git a/gazelle/README.md b/gazelle/README.md
index 58ec55e..5c63e21 100644
--- a/gazelle/README.md
+++ b/gazelle/README.md
@@ -222,6 +222,8 @@
| 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:experimental_allow_relative_imports` | `false` |
| Controls whether Gazelle resolves dependencies for import statements that use paths relative to the current package. Can be "true" or "false".|
+| `# 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 ae0f7ee..db80fc1 100644
--- a/gazelle/python/configure.go
+++ b/gazelle/python/configure.go
@@ -68,6 +68,7 @@
pythonconfig.TestFilePattern,
pythonconfig.LabelConvention,
pythonconfig.LabelNormalization,
+ pythonconfig.GeneratePyiDeps,
pythonconfig.ExperimentalAllowRelativeImports,
}
}
@@ -230,6 +231,12 @@
pythonconfig.ExperimentalAllowRelativeImports, rel, d.Value)
}
config.SetExperimentalAllowRelativeImports(v)
+ 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/file_parser.go b/gazelle/python/file_parser.go
index cb82cb9..aca925c 100644
--- a/gazelle/python/file_parser.go
+++ b/gazelle/python/file_parser.go
@@ -47,9 +47,10 @@
}
type FileParser struct {
- code []byte
- relFilepath string
- output ParserOutput
+ code []byte
+ relFilepath string
+ output ParserOutput
+ inTypeCheckingBlock bool
}
func NewFileParser() *FileParser {
@@ -158,6 +159,7 @@
continue
}
m.Filepath = p.relFilepath
+ m.TypeCheckingOnly = p.inTypeCheckingBlock
if strings.HasPrefix(m.Name, ".") {
continue
}
@@ -178,6 +180,7 @@
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 {
@@ -202,10 +205,43 @@
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
@@ -219,6 +255,9 @@
}
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 20085f0..f4db1a3 100644
--- a/gazelle/python/file_parser_test.go
+++ b/gazelle/python/file_parser_test.go
@@ -254,3 +254,40 @@
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 cf80578..11e01db 100644
--- a/gazelle/python/parser.go
+++ b/gazelle/python/parser.go
@@ -112,9 +112,9 @@
continue
}
- modules.Add(m)
+ addModuleToTreeSet(modules, m)
if res.HasMain {
- mainModules[res.FileName].Add(m)
+ addModuleToTreeSet(mainModules[res.FileName], m)
}
}
@@ -158,6 +158,8 @@
// 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.
@@ -165,6 +167,15 @@
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/resolve.go b/gazelle/python/resolve.go
index 413e69b..88275e0 100644
--- a/gazelle/python/resolve.go
+++ b/gazelle/python/resolve.go
@@ -123,6 +123,16 @@
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,9 +151,11 @@
// 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)
+ 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()
@@ -228,7 +240,7 @@
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, "+
@@ -239,7 +251,7 @@
}
} 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)),
@@ -249,7 +261,8 @@
}
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 {
@@ -308,7 +321,7 @@
}
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, "+
@@ -333,6 +346,34 @@
os.Exit(1)
}
}
+
+ addResolvedDeps(r, deps)
+
+ if cfg.GeneratePyiDeps() {
+ 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()...)
+
+ if !combinedDeps.Empty() {
+ r.SetAttr("deps", convertDependencySetToExpr(combinedDeps))
+ }
+ }
+}
+
+// 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,
+) {
resolvedDeps := r.PrivateAttr(resolvedDepsKey).(*treeset.Set)
if !resolvedDeps.Empty() {
it := resolvedDeps.Iterator()
@@ -340,9 +381,6 @@
deps.Add(it.Value())
}
}
- if !deps.Empty() {
- r.SetAttr("deps", 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 1fb9218..06b653d 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.
@@ -79,7 +80,8 @@
// dependency resolution easier
dep.Name = importSpecFromSrc(t.pythonProjectRoot, t.bzlPackage, fileName).Imp
}
- t.deps.Add(dep)
+
+ addModuleToTreeSet(t.deps, dep)
return t
}
diff --git a/gazelle/python/testdata/add_type_stub_packages/BUILD.in b/gazelle/python/testdata/add_type_stub_packages/BUILD.in
index e69de29..99d122a 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 d30540f..1a5b640 100644
--- a/gazelle/python/testdata/add_type_stub_packages/BUILD.out
+++ b/gazelle/python/testdata/add_type_stub_packages/BUILD.out
@@ -1,14 +1,18 @@
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"],
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/add_type_stub_packages/README.md b/gazelle/python/testdata/add_type_stub_packages/README.md
index c42e76f..e3a2afe 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.
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 0000000..d4dce06
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports/BUILD.in
@@ -0,0 +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
new file mode 100644
index 0000000..6902106
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports/BUILD.out
@@ -0,0 +1,33 @@
+load("@rules_python//python:defs.bzl", "py_library")
+
+# gazelle:python_generation_mode file
+# gazelle:python_generate_pyi_deps true
+
+py_library(
+ name = "bar",
+ srcs = ["bar.py"],
+ pyi_deps = [":foo"],
+ visibility = ["//:__subpackages__"],
+ deps = [":baz"],
+)
+
+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//boto3_stubs",
+ "@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 0000000..b09f442
--- /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 0000000..3e6e74e
--- /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 0000000..47c7d93
--- /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 0000000..1c69e25
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports/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/foo.py b/gazelle/python/testdata/type_checking_imports/foo.py
new file mode 100644
index 0000000..655cb54
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports/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/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports/gazelle_python.yaml
new file mode 100644
index 0000000..a782354
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports/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/test.yaml b/gazelle/python/testdata/type_checking_imports/test.yaml
new file mode 100644
index 0000000..fcea777
--- /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.
+
+---
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 0000000..ab6d30f
--- /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 0000000..bf23d28
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports_disabled/BUILD.out
@@ -0,0 +1,35 @@
+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__"],
+ deps = [
+ "@gazelle_python_test//boto3",
+ "@gazelle_python_test//boto3_stubs",
+ ],
+)
+
+py_library(
+ name = "foo",
+ srcs = ["foo.py"],
+ 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/README.md b/gazelle/python/testdata/type_checking_imports_disabled/README.md
new file mode 100644
index 0000000..0e3b623
--- /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 0000000..3e6e74e
--- /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 0000000..47c7d93
--- /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 0000000..1c69e25
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports_disabled/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_disabled/foo.py b/gazelle/python/testdata/type_checking_imports_disabled/foo.py
new file mode 100644
index 0000000..655cb54
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports_disabled/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_disabled/gazelle_python.yaml b/gazelle/python/testdata/type_checking_imports_disabled/gazelle_python.yaml
new file mode 100644
index 0000000..a782354
--- /dev/null
+++ b/gazelle/python/testdata/type_checking_imports_disabled/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_disabled/test.yaml b/gazelle/python/testdata/type_checking_imports_disabled/test.yaml
new file mode 100644
index 0000000..fcea777
--- /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/python/testdata/type_checking_imports_package/BUILD.in b/gazelle/python/testdata/type_checking_imports_package/BUILD.in
new file mode 100644
index 0000000..8e6c1cb
--- /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 0000000..0091e9c
--- /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 0000000..3e2cafe
--- /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 0000000..3e6e74e
--- /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 0000000..47c7d93
--- /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 0000000..1c69e25
--- /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 0000000..655cb54
--- /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 0000000..a782354
--- /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 0000000..fcea777
--- /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 0000000..808e3e0
--- /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 0000000..6d6ac3c
--- /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 0000000..ead09e1
--- /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 0000000..3e6e74e
--- /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 0000000..47c7d93
--- /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 0000000..1c69e25
--- /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 0000000..655cb54
--- /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 0000000..a782354
--- /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 0000000..fcea777
--- /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.
+
+---
diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go
index e0a2b8a..8bf79cb 100644
--- a/gazelle/pythonconfig/pythonconfig.go
+++ b/gazelle/pythonconfig/pythonconfig.go
@@ -94,6 +94,10 @@
// ExperimentalAllowRelativeImports represents the directive that controls
// whether relative imports are allowed.
ExperimentalAllowRelativeImports = "experimental_allow_relative_imports"
+ // 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
@@ -181,6 +185,7 @@
labelConvention string
labelNormalization LabelNormalizationType
experimentalAllowRelativeImports bool
+ generatePyiDeps bool
}
type LabelNormalizationType int
@@ -217,6 +222,7 @@
labelConvention: DefaultLabelConvention,
labelNormalization: DefaultLabelNormalizationType,
experimentalAllowRelativeImports: false,
+ generatePyiDeps: false,
}
}
@@ -250,6 +256,7 @@
labelConvention: c.labelConvention,
labelNormalization: c.labelNormalization,
experimentalAllowRelativeImports: c.experimentalAllowRelativeImports,
+ generatePyiDeps: c.generatePyiDeps,
}
}
@@ -536,6 +543,18 @@
return c.experimentalAllowRelativeImports
}
+// 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)