Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(gazelle): Directive controlling pytest ancestor dependencies by dougthor42 · Pull Request #3596 · bazel-contrib/rules_python · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,7 +82,9 @@ END_UNRELEASED_TEMPLATE
* (tests) No more coverage warnings are being printed if there are no sources.
([#2762](https://github.com/bazel-contrib/rules_python/issues/2762))
* (gazelle) Ancestor `conftest.py` files are added in addition to sibling `conftest.py`.
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497))
([#3497](https://github.com/bazel-contrib/rules_python/issues/3497)) Note
that this behavior can be reverted to the pre-VERSION_NEXT_FEATURE behavior by setting the new
`python_include_ancestor_conftest` directive to `false`.
* (pypi) `pip_parse` no longer silently drops PEP 508 URL-based requirements
(`pkg @ https://...`) when `extract_url_srcs=False` (the default for
`pip_repository`).
Expand DownExpand Up@@ -115,6 +117,12 @@ END_UNRELEASED_TEMPLATE
{obj}`PyExecutableInfo.venv_python_exe`.
* (tools/wheelmaker.py) Added support for URL requirements according to PEP 508
in Requires-Dist metadata. ([#3569](https://github.com/bazel-contrib/rules_python/pull/3569))
* (gazelle) A new directive `python_include_ancestor_conftest` has been added.
When `false`, ancestor `conftest` targets are not automatically added to
{bzl:obj}`py_test` target dependencies. This `false` behavior is how things
were in `rules_python` before VERSION_NEXT_FEATURE. The default is `true`, as the prior behavior
was technically incorrect.
([#3596](https://github.com/bazel-contrib/rules_python/pull/3596))

{#v1-8-4}
## [1.8.4] - 2026-02-10
Expand Down
1 change: 1 addition & 0 deletions gazelle/docs/annotations.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,7 @@ deps = [
```


(annotation-include-pytest-conftest)=
## `include_pytest_conftest`

:::{versionadded} 1.6.0
Expand Down
69 changes: 69 additions & 0 deletions gazelle/docs/directives.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -175,6 +175,11 @@ The Python-specific directives are:
* Default: `false`
* Allowed Values: `true`, `false`

[`# gazelle:python_include_ancestor_conftest bool`](#python-include-ancestor-conftest)
: Controls whether ancestor conftest targets are added to {bzl:obj}`py_test` target
dependencies.
* Default: `true`
* Allowed Values: `true`, `false`

## `python_extension`

Expand DownExpand Up@@ -720,3 +725,67 @@ previously-generated or hand-created rules.
:::{error}
Detailed docs are not yet written.
:::

## `python_include_ancestor_conftest`

Version VERSION_NEXT_FEATURE includes a fix ({gh-pr}`3498`) for a long-standing issue
({gh-issue}`3497`) where ancestor `conftest.py` files were not automatically
added as dependencies of {bzl:obj}`py_test` targets.

However, some people may not want this behavior (see https://xkcd.com/1172/).
Thus the `python_include_ancestor_conftest` directive controls this behavior.
It defaults to `true`, which causes all ancestor `conftest.py` files to be
included as dependencies for {bzl:obj}`py_test` targets.

Setting the directive to `false` reverts to the pre-VERSION_NEXT_FEATURE behavior.

For example, given this directory tree (not shown: intermediary `BUILD.bazel`
files)

```
./
├── conftest.py
└── one/
├── conftest.py
└── two/
├── conftest.py
└── three/
├── BUILD.bazel
├── conftest.py
└── my_test.py
```

Gazelle will generate this target for `foo_test.py` by default:

```starlark
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest", # same as "//one:two/three:conftest"
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
```

But when `python_include_ancestor_conftest` is `false`, only the sibling
`:conftest` target will be included as a dependency:

:::{tip}
The [`include_pytest_conftest` annotation](annotation-include-pytest-conftest)
controls whether the sibling `:conftest` target is added to {bzl:obj}`py_test`
target dependency list.
:::

```starlark
# gazelle:python_include_ancestor_conftest false
py_test(
name = "foo_test",
srcs = ["foo_test.py"],
deps = [
":conftest",
],
)
```
7 changes: 7 additions & 0 deletions gazelle/python/configure.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,6 +74,7 @@ func (py *Configurer) KnownDirectives() []string {
pythonconfig.ExperimentalAllowRelativeImports,
pythonconfig.GenerateProto,
pythonconfig.PythonResolveSiblingImports,
pythonconfig.PythonIncludeAncestorConftest,
}
}

Expand DownExpand Up@@ -261,6 +262,12 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) {
log.Fatal(err)
}
config.SetResolveSiblingImports(v)
case pythonconfig.PythonIncludeAncestorConftest:
v, err := strconv.ParseBool(strings.TrimSpace(d.Value))
if err != nil {
log.Fatal(err)
}
config.SetIncludeAncestorConftest(v)
Comment thread
dougthor42 marked this conversation as resolved.
}
}

Expand Down
15 changes: 10 additions & 5 deletions gazelle/python/generate.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,7 +68,7 @@ func matchesAnyGlob(s string, globs []string) bool {

// findConftestPaths returns package paths containing conftest.py, from currentPkg
// up through ancestors, stopping at module root.
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string {
func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string, includeAncestorConftest bool) []string {
var result []string
for pkg := currentPkg; ; pkg = filepath.Dir(pkg) {
if pkg == "." {
Expand All@@ -77,6 +77,12 @@ func findConftestPaths(repoRoot, currentPkg, pythonProjectRoot string) []string
if _, err := os.Stat(filepath.Join(repoRoot, pkg, conftestFilename)); err == nil {
result = append(result, pkg)
}
// We traverse up the tree to find conftest files and we start in
// the current package. Thus if we find one in the current package
// and do not want ancestors, we break early.
if !includeAncestorConftest {

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: I opted to keep this separate from the next if to make things more readable. LMK if anyone thinks otherwise:

ifpkg==""or!includeAncestorConftest {
break
}

break
}
if pkg == "" {
break
}
Expand DownExpand Up@@ -402,7 +408,6 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes
setAnnotations(*annotations).
generateImportsAttribute()


pyBinary := pyBinaryTarget.build()

result.Gen = append(result.Gen, pyBinary)
Expand DownExpand Up@@ -526,13 +531,13 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes

for _, pyTestTarget := range pyTestTargets {
shouldAddConftest := pyTestTarget.annotations.includePytestConftest == nil ||
*pyTestTarget.annotations.includePytestConftest
*pyTestTarget.annotations.includePytestConftest

if shouldAddConftest {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot) {
for _, conftestPkg := range findConftestPaths(args.Config.RepoRoot, args.Rel, pythonProjectRoot, cfg.IncludeAncestorConftest()) {
pyTestTarget.addModuleDependency(
Module{
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Name: importSpecFromSrc(pythonProjectRoot, conftestPkg, conftestFilename).Imp,
Filepath: filepath.Join(conftestPkg, conftestFilename),
},
)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:defs.bzl", "py_library")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
# Directive: `python_include_ancestor_conftest`

This test case asserts that the `# gazelle:python_include_ancestor_conftest`
directive correctly includes or excludes ancestor `conftest` targets in
`py_test` target dependencies.

The test also asserts that the directive can be applied at any level and that
child levels will inherit the value:

+ The root level does not set the directive (it defaults to True).
+ The next level, `one/`, inherits that value.
+ The next level, `one/two/`, sets the directive to False; consequently the
`py_test` target only includes the sibling `:conftest` target.
+ The `one/two/no_conftest/` directory does not contain a `conftest.py` file
thereby asserting that we correctly do not include any `conftest` targets
whatsoever.
+ The final level, `one/two/three/`, sets the directive back to True, meaning
the `py_test` target includes a total of 4 `conftest` targets.
+ The `one/two/three/no_conftest/` directory does not contain a `conftest.py`
file and thus asserts that the code includes _only_ ancestor `conftest`

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, I really like the phrase "and thus" ...

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had gemini provide some synonym phrases.

targets.

See [Issue #3595](https://github.com/bazel-contrib/rules_python/issues/3595).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest false

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [":conftest"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# gazelle:python_include_ancestor_conftest true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
load("@rules_python//python:defs.bzl", "py_library", "py_test")

# gazelle:python_include_ancestor_conftest true

py_library(
name = "conftest",
testonly = True,
srcs = ["conftest.py"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
":conftest",
"//:conftest",
"//one:conftest",
"//one/two:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
load("@rules_python//python:defs.bzl", "py_test")

py_test(
name = "my_test",
srcs = ["my_test.py"],
deps = [
"//:conftest",
"//one:conftest",
"//one/two:conftest",
"//one/two/three:conftest",
],
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
---
expect:
exit_code: 0
22 changes: 22 additions & 0 deletions gazelle/pythonconfig/pythonconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,6 +116,15 @@ const (
// like "import a" can be resolved to sibling modules. When disabled, they
// can only be resolved as an absolute import.
PythonResolveSiblingImports = "python_resolve_sibling_imports"
// PythonIncludeAncestorConftest represents the directive that controls
// whether ancestor conftest.py files are added as dependencies to py_test
// targets. When enabled (the default), ancestor conftest.py files are
// included as deps.
// See also https://github.com/bazel-contrib/rules_python/pull/3498, which
// fixed previous behavior that was incorrectly _not_ adding the files and
// https://github.com/bazel-contrib/rules_python/issues/3595 which requested
// that the behavior be configurable.
PythonIncludeAncestorConftest = "python_include_ancestor_conftest"
)

// GenerationModeType represents one of the generation modes for the Python
Expand DownExpand Up@@ -209,6 +218,7 @@ type Config struct {
generatePyiSrcs bool
generateProto bool
resolveSiblingImports bool
includeAncestorConftest bool
}

type LabelNormalizationType int
Expand DownExpand Up@@ -250,6 +260,7 @@ func New(
generatePyiSrcs: false,
generateProto: false,
resolveSiblingImports: false,
includeAncestorConftest: true,
}
}

Expand DownExpand Up@@ -288,6 +299,7 @@ func (c *Config) NewChild() *Config {
generatePyiSrcs: c.generatePyiSrcs,
generateProto: c.generateProto,
resolveSiblingImports: c.resolveSiblingImports,
includeAncestorConftest: c.includeAncestorConftest,
}
}

Expand DownExpand Up@@ -629,6 +641,16 @@ func (c *Config) ResolveSiblingImports() bool {
return c.resolveSiblingImports
}

// SetIncludeAncestorConftest sets whether ancestor conftest files are added to py_test targets.
func (c *Config) SetIncludeAncestorConftest(includeAncestorConftest bool) {
c.includeAncestorConftest = includeAncestorConftest
}

// IncludeAncestorConftest returns whether ancestor conftest files are added to py_test targets.
func (c *Config) IncludeAncestorConftest() bool {
return c.includeAncestorConftest
}

// 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)
Expand Down