Skip to content

Commit 5e0b96d

Browse files
authored
Merge branch 'main' into gazelle-plugin-proto-naming-convention
2 parents b0e51b6 + 9555ba8 commit 5e0b96d

31 files changed

+413
-120
lines changed

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ END_UNRELEASED_TEMPLATE
6060
* (gazelle) Types for exposed members of `python.ParserOutput` are now all public.
6161
* (gazelle) Removed the requirement for `__init__.py`, `__main__.py`, or `__test__.py` files to be
6262
present in a directory to generate a `BUILD.bazel` file.
63-
* (toolchain) Updated the following toolchains to build 20250702 to patch CVE-2025-47273:
63+
* (toolchain) Updated the following toolchains to build 20250708 to patch CVE-2025-47273:
6464
* 3.9.23
6565
* 3.10.18
6666
* 3.11.13
6767
* 3.12.11
68-
* 3.14.0b3
68+
* 3.14.0b4
6969
* (toolchain) Python 3.13 now references 3.13.5
7070
* (gazelle) Switched back to smacker/go-tree-sitter, fixing
7171
[#2630](https://github.com/bazel-contrib/rules_python/issues/2630)
@@ -105,7 +105,12 @@ END_UNRELEASED_TEMPLATE
105105
* 3.11.13
106106
* 3.12.11
107107
* 3.13.5
108-
* 3.14.0b3
108+
* 3.14.0b4
109+
* (gazelle): New annotation `gazelle:include_pytest_conftest`. When not set (the
110+
default) or `true`, gazelle will inject any `conftest.py` file found in the same
111+
directory as a {obj}`py_test` target to that {obj}`py_test` target's `deps`.
112+
This behavior is unchanged from previous versions. When `false`, the `:conftest`
113+
dep is not added to the {obj}`py_test` target.
109114
* (gazelle) New directive `gazelle:python_generate_proto`; when `true`,
110115
Gazelle generates `py_proto_library` rules for `proto_library`. `false` by default.
111116
* (gazelle) New directive `gazelle:python_proto_naming_convention`; controls

docs/pypi/lock.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
:::{note}
77
Currently `rules_python` only supports `requirements.txt` format.
8+
9+
#{gh-issue}`2787` tracks `pylock.toml` support.
810
:::
911

1012
## requirements.txt
@@ -37,11 +39,33 @@ This rule generates two targets:
3739
Once you generate this fully specified list of requirements, you can install the requirements ([bzlmod](./download)/[WORKSPACE](./download-workspace)).
3840

3941
:::{warning}
40-
If you're specifying dependencies in `pyproject.toml`, make sure to include the `[build-system]` configuration, with pinned dependencies. `compile_pip_requirements` will use the build system specified to read your project's metadata, and you might see non-hermetic behavior if you don't pin the build system.
42+
If you're specifying dependencies in `pyproject.toml`, make sure to include the
43+
`[build-system]` configuration, with pinned dependencies.
44+
`compile_pip_requirements` will use the build system specified to read your
45+
project's metadata, and you might see non-hermetic behavior if you don't pin the
46+
build system.
4147

42-
Not specifying `[build-system]` at all will result in using a default `[build-system]` configuration, which uses unpinned versions ([ref](https://peps.python.org/pep-0518/#build-system-table)).
48+
Not specifying `[build-system]` at all will result in using a default
49+
`[build-system]` configuration, which uses unpinned versions
50+
([ref](https://peps.python.org/pep-0518/#build-system-table)).
4351
:::
4452

53+
54+
#### pip compile Dependency groups
55+
56+
pip-compile doesn't yet support pyproject.toml dependency groups. Follow
57+
[pip-tools #2062](https://github.com/jazzband/pip-tools/issues/2062)
58+
to see the status of their support.
59+
60+
In the meantime, support can be emulated by passing multiple files to `srcs`:
61+
62+
```starlark
63+
compile_pip_requirements(
64+
srcs = ["pyproject.toml", "requirements-dev.in"]
65+
...
66+
)
67+
```
68+
4569
### uv pip compile (bzlmod only)
4670

4771
We also have experimental setup for the `uv pip compile` way of generating lock files.

gazelle/README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,8 @@ The annotations are:
552552
| Tells Gazelle to ignore import statements. `imports` is a comma-separated list of imports to ignore. | |
553553
| [`# gazelle:include_dep targets`](#annotation-include_dep) | N/A |
554554
| Tells Gazelle to include a set of dependencies, even if they are not imported in a Python module. `targets` is a comma-separated list of target names to include as dependencies. | |
555+
| [`# gazelle:include_pytest_conftest bool`](#annotation-include_pytest_conftest) | N/A |
556+
| Whether or not to include a sibling `:conftest` target in the deps of a `py_test` target. Default behaviour is to include `:conftest`. | |
555557

556558

557559
#### Annotation: `ignore`
@@ -624,6 +626,89 @@ deps = [
624626
]
625627
```
626628

629+
#### Annotation: `include_pytest_conftest`
630+
631+
Added in [#3080][gh3080].
632+
633+
[gh3080]: https://github.com/bazel-contrib/rules_python/pull/3080
634+
635+
This annotation accepts any string that can be parsed by go's
636+
[`strconv.ParseBool`][ParseBool]. If an unparsable string is passed, the
637+
annotation is ignored.
638+
639+
[ParseBool]: https://pkg.go.dev/strconv#ParseBool
640+
641+
Starting with [`rules_python` 0.14.0][rules-python-0.14.0] (specifically [PR #879][gh879]),
642+
Gazelle will include a `:conftest` dependency to an `py_test` target that is in
643+
the same directory as `conftest.py`.
644+
645+
[rules-python-0.14.0]: https://github.com/bazel-contrib/rules_python/releases/tag/0.14.0
646+
[gh879]: https://github.com/bazel-contrib/rules_python/pull/879
647+
648+
This annotation allows users to adjust that behavior. To disable the behavior, set
649+
the annotation value to "false":
650+
651+
```
652+
# some_file_test.py
653+
# gazelle:include_pytest_conftest false
654+
```
655+
656+
Example:
657+
658+
Given a directory tree like:
659+
660+
```
661+
.
662+
├── BUILD.bazel
663+
├── conftest.py
664+
└── some_file_test.py
665+
```
666+
667+
The default Gazelle behavior would create:
668+
669+
```starlark
670+
py_library(
671+
name = "conftest",
672+
testonly = True,
673+
srcs = ["conftest.py"],
674+
visibility = ["//:__subpackages__"],
675+
)
676+
677+
py_test(
678+
name = "some_file_test",
679+
srcs = ["some_file_test.py"],
680+
deps = [":conftest"],
681+
)
682+
```
683+
684+
When `# gazelle:include_pytest_conftest false` is found in `some_file_test.py`
685+
686+
```python
687+
# some_file_test.py
688+
# gazelle:include_pytest_conftest false
689+
```
690+
691+
Gazelle will generate:
692+
693+
```starlark
694+
py_library(
695+
name = "conftest",
696+
testonly = True,
697+
srcs = ["conftest.py"],
698+
visibility = ["//:__subpackages__"],
699+
)
700+
701+
py_test(
702+
name = "some_file_test",
703+
srcs = ["some_file_test.py"],
704+
)
705+
```
706+
707+
See [Issue #3076][gh3076] for more information.
708+
709+
[gh3076]: https://github.com/bazel-contrib/rules_python/issues/3076
710+
711+
627712
#### Directive: `experimental_allow_relative_imports`
628713
Enables experimental support for resolving relative imports in
629714
`python_generation_mode package`.

gazelle/python/generate.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,9 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
264264
addSrc(filename).
265265
addModuleDependencies(mainModules[filename]).
266266
addResolvedDependencies(annotations.includeDeps).
267-
generateImportsAttribute().build()
267+
generateImportsAttribute().
268+
setAnnotations(*annotations).
269+
build()
268270
result.Gen = append(result.Gen, pyBinary)
269271
result.Imports = append(result.Imports, pyBinary.PrivateAttr(config.GazelleImportsKey))
270272
}
@@ -305,6 +307,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
305307
addModuleDependencies(allDeps).
306308
addResolvedDependencies(annotations.includeDeps).
307309
generateImportsAttribute().
310+
setAnnotations(*annotations).
308311
build()
309312

310313
if pyLibrary.IsEmpty(py.Kinds()[pyLibrary.Kind()]) {
@@ -357,6 +360,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
357360
addSrc(pyBinaryEntrypointFilename).
358361
addModuleDependencies(deps).
359362
addResolvedDependencies(annotations.includeDeps).
363+
setAnnotations(*annotations).
360364
generateImportsAttribute()
361365

362366
pyBinary := pyBinaryTarget.build()
@@ -387,6 +391,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
387391
addSrc(conftestFilename).
388392
addModuleDependencies(deps).
389393
addResolvedDependencies(annotations.includeDeps).
394+
setAnnotations(*annotations).
390395
addVisibility(visibility).
391396
setTestonly().
392397
generateImportsAttribute()
@@ -418,6 +423,7 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
418423
addSrcs(srcs).
419424
addModuleDependencies(deps).
420425
addResolvedDependencies(annotations.includeDeps).
426+
setAnnotations(*annotations).
421427
generateImportsAttribute()
422428
}
423429
if (!cfg.PerPackageGenerationRequireTestEntryPoint() || hasPyTestEntryPointFile || hasPyTestEntryPointTarget || cfg.CoarseGrainedGeneration()) && !cfg.PerFileGeneration() {
@@ -470,7 +476,14 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
470476

471477
for _, pyTestTarget := range pyTestTargets {
472478
if conftest != nil {
473-
pyTestTarget.addModuleDependency(Module{Name: strings.TrimSuffix(conftestFilename, ".py")})
479+
conftestModule := Module{Name: strings.TrimSuffix(conftestFilename, ".py")}
480+
if pyTestTarget.annotations.includePytestConftest == nil {
481+
// unset; default behavior
482+
pyTestTarget.addModuleDependency(conftestModule)
483+
} else if *pyTestTarget.annotations.includePytestConftest {
484+
// set; add if true, do not add if false
485+
pyTestTarget.addModuleDependency(conftestModule)
486+
}
474487
}
475488
pyTest := pyTestTarget.build()
476489

gazelle/python/parser.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"context"
1919
_ "embed"
2020
"fmt"
21+
"log"
22+
"strconv"
2123
"strings"
2224

2325
"github.com/emirpasic/gods/sets/treeset"
@@ -123,6 +125,7 @@ func (p *python3Parser) parse(pyFilenames *treeset.Set) (*treeset.Set, map[strin
123125
allAnnotations.ignore[k] = v
124126
}
125127
allAnnotations.includeDeps = append(allAnnotations.includeDeps, annotations.includeDeps...)
128+
allAnnotations.includePytestConftest = annotations.includePytestConftest
126129
}
127130

128131
allAnnotations.includeDeps = removeDupesFromStringTreeSetSlice(allAnnotations.includeDeps)
@@ -183,8 +186,12 @@ const (
183186
// The Gazelle annotation prefix.
184187
annotationPrefix string = "gazelle:"
185188
// The ignore annotation kind. E.g. '# gazelle:ignore <module_name>'.
186-
annotationKindIgnore annotationKind = "ignore"
187-
annotationKindIncludeDep annotationKind = "include_dep"
189+
annotationKindIgnore annotationKind = "ignore"
190+
// Force a particular target to be added to `deps`. Multiple invocations are
191+
// accumulated and the value can be comma separated.
192+
// Eg: '# gazelle:include_dep //foo/bar:baz,@repo//:target
193+
annotationKindIncludeDep annotationKind = "include_dep"
194+
annotationKindIncludePytestConftest annotationKind = "include_pytest_conftest"
188195
)
189196

190197
// Comment represents a Python comment.
@@ -222,13 +229,18 @@ type annotations struct {
222229
ignore map[string]struct{}
223230
// Labels that Gazelle should include as deps of the generated target.
224231
includeDeps []string
232+
// Whether the conftest.py file, found in the same directory as the current
233+
// python test file, should be added to the py_test target's `deps` attribute.
234+
// A *bool is used so that we can handle the "not set" state.
235+
includePytestConftest *bool
225236
}
226237

227238
// annotationsFromComments returns all the annotations parsed out of the
228239
// comments of a Python module.
229240
func annotationsFromComments(comments []Comment) (*annotations, error) {
230241
ignore := make(map[string]struct{})
231242
includeDeps := []string{}
243+
var includePytestConftest *bool
232244
for _, comment := range comments {
233245
annotation, err := comment.asAnnotation()
234246
if err != nil {
@@ -255,11 +267,21 @@ func annotationsFromComments(comments []Comment) (*annotations, error) {
255267
includeDeps = append(includeDeps, t)
256268
}
257269
}
270+
if annotation.kind == annotationKindIncludePytestConftest {
271+
val := annotation.value
272+
parsedVal, err := strconv.ParseBool(val)
273+
if err != nil {
274+
log.Printf("WARNING: unable to cast %q to bool in %q. Ignoring annotation", val, comment)
275+
continue
276+
}
277+
includePytestConftest = &parsedVal
278+
}
258279
}
259280
}
260281
return &annotations{
261-
ignore: ignore,
262-
includeDeps: includeDeps,
282+
ignore: ignore,
283+
includeDeps: includeDeps,
284+
includePytestConftest: includePytestConftest,
263285
}, nil
264286
}
265287

gazelle/python/target.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type targetBuilder struct {
3737
main *string
3838
imports []string
3939
testonly bool
40+
annotations *annotations
4041
}
4142

4243
// newTargetBuilder constructs a new targetBuilder.
@@ -51,6 +52,7 @@ func newTargetBuilder(kind, name, pythonProjectRoot, bzlPackage string, siblingS
5152
deps: treeset.NewWith(moduleComparator),
5253
resolvedDeps: treeset.NewWith(godsutils.StringComparator),
5354
visibility: treeset.NewWith(godsutils.StringComparator),
55+
annotations: new(annotations),
5456
}
5557
}
5658

@@ -130,6 +132,13 @@ func (t *targetBuilder) setTestonly() *targetBuilder {
130132
return t
131133
}
132134

135+
// setAnnotations sets the annotations attribute on the target.
136+
func (t *targetBuilder) setAnnotations(val annotations) *targetBuilder {
137+
t.annotations = &val
138+
return t
139+
}
140+
141+
133142
// generateImportsAttribute generates the imports attribute.
134143
// These are a list of import directories to be added to the PYTHONPATH. In our
135144
// case, the value we add is on Bazel sub-packages to be able to perform imports
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Annotation: Include Pytest Conftest
2+
3+
Validate that the `# gazelle:include_pytest_conftest` annotation follows
4+
this logic:
5+
6+
+ When a `conftest.py` file does not exist:
7+
+ all values have no affect
8+
+ When a `conftest.py` file does exist:
9+
+ Truthy values add `:conftest` to `deps`.
10+
+ Falsey values do not add `:conftest` to `deps`.
11+
+ Unset (no annotation) performs the default action.
12+
13+
Additionally, we test that:
14+
15+
+ invalid values (eg `foo`) print a warning and then act as if
16+
the annotation was not present.
17+
+ last annotation (highest line number) wins.
18+
+ the annotation has no effect on non-test files/targets.
19+
+ the `include_dep` can still inject `:conftest` even when `include_pytest_conftest`
20+
is false.
21+
+ `import conftest` will still add the dep even when `include_pytest_conftest` is
22+
false.
23+
24+
An annotation without a value is not tested, as that's part of the core
25+
annotation framework and not specific to this annotation.

gazelle/python/testdata/annotation_include_pytest_conftest/WORKSPACE

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
expect:
3+
stderr: |
4+
gazelle: WARNING: unable to cast "foo" to bool in "# gazelle:include_pytest_conftest foo". Ignoring annotation
5+
exit_code: 0

gazelle/python/testdata/annotation_include_pytest_conftest/with_conftest/BUILD.in

Whitespace-only changes.

0 commit comments

Comments
 (0)