downstream test: varnames fix on py3.14 - #4
Conversation
updates: - [github.com/pre-commit/mirrors-mypy: v1.20.2 → v2.0.0](pre-commit/mirrors-mypy@v1.20.2...v2.0.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Replace per-project shell scripts with a PEP 723 uv-runnable driver and validated recipe files under downstream/recipes/. Environment kinds (uv-venv, stdlib-venv, none) pair with nested [environment.install] in each TOML. Document the flow in downstream/README.md and RELEASING.rst. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Composer <composer@cursor.com>
Introduce a workflow_dispatch-only workflow that runs each downstream TOML recipe (conda, datasette, devpi, hatch, pytest, python-lsp-server, tox) on ubuntu-latest with Python 3.12 and uv. Matrix jobs are independent (fail-fast: false) and time out after 120 minutes. Trigger from Actions and, when validating a change that needs it, select the relevant branch under "Use workflow from" (for example a pull request head branch). Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Composer <composer@cursor.com>
workflow_dispatch alone does not show checks on a pull request; add a label-gated pull_request trigger (labeled, synchronize) so maintainers can apply run-downstream to opt in. Document that the workflow file must exist on the default branch to appear under upstream Actions. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Composer <composer@cursor.com>
Replace the run-downstream label with a head branch filter so forks without shared labels still opt in (e.g. downstream-driver). Pull requests still target main only. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Composer <composer@cursor.com>
actions/checkout defaults to a shallow clone; pluggy installs as -e ../.. need a real tree (e.g. setuptools-scm). Set fetch-depth: 0. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Composer <composer@cursor.com>
Hatch's backend tests use the pytest-mock "mocker" fixture; include it in the extra uv packages so downstream CI matches a full dev install. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Composer <composer@cursor.com>
- Add env field to TestStep for per-step environment overrides - tox: full clone (fixes 0.1.dev1 version) + CI=false (suppresses list_dependencies/freeze steps that break test expectations) - conda: full clone (fixes vcs_versioning shallow warnings) + CONDA_CHANNELS=defaults,conda-forge for channel-dependent tests - devpi: deselect test_upload.py (upstream conftest bug comparing list code=[200,200,200] against integers) - python-lsp-server: deselect 2 jedi tests needing /tmp/pyenv/ Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
… hack - tox: also override GITHUB_ACTIONS=false (is_ci() checks both CI and GITHUB_ACTIONS env vars) - devpi: switch --deselect to --ignore for test_upload.py (deselect path didn't match pytest node IDs) - conda: remove CONDA_CHANNELS override — it fixes NoChannels tests but breaks channel-configuration tests; remaining failures are upstream conda CI infrastructure issues Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
tox's is_ci() checks presence of CI (any value) and GITHUB_ACTIONS==true. Setting CI=false still leaves it present, so is_ci() returns True. Now empty-string env values mean "remove from environment" in the driver, and tox recipe uses CI="" and GITHUB_ACTIONS="" to fully unset them. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
conda's own CI uses condarc-file to set channels; dev/start alone doesn't write one, so tests creating temporary envs fail with NoChannelsConfiguredError. Add `conda config --add channels defaults` after bootstrap, matching their CI's condarc-defaults configuration. Unlike the CONDA_CHANNELS env var (which conflicted with channel-config tests), writing to .condarc is the proper mechanism that conda's test fixtures handle correctly. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
test_export_from_history_format fails because our pluggy is pip-installed (editable) into the conda env rather than conda-installed, so it's missing from conda's explicit_packages. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Drop stdlib-venv and EnvironmentNone/bootstrap in favour of two environment kinds: uv-venv (venv + uv pip install) and script (self-contained bash script handling the full workflow). - Move conda's inline bash into recipes/conda.bash - Switch pytest recipe from stdlib-venv to uv-venv - Remove PipInstallOptions, StdlibInstall, NoneInstall, BootstrapConfig and related helpers (-104 lines of Python) Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
…cture Drop the `kind` discriminator field from environment configs. The two environment types are now distinguished by their keys: - `editables` present → uv-venv (creates venv, installs via uv pip) - `run` present → script (delegates to bash) Fold `[environment.install]` and `[environment.install.uv]` sub-tables directly into `[environment]`, removing UvInstall/UvInstallOptions models. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Drop unnecessary dict(), list(), and type annotations where the types are already correct or inferred from context. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
The previous cleanup accidentally dropped the list() wrapper around env.items(), causing a RuntimeError when empty-string values were deleted during iteration. Replace with a dict comprehension instead. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Co-authored-by: nightcityblade <nightcityblade@gmail.com>
Co-authored-by: nightcityblade <nightcityblade@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
Revert the review suggestion to use a temporary CONDARC file. Exporting CONDARC overrides the devenv's own .condarc, breaking the bootstrap. Go back to the simple conda config --add that worked. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
…river rewired downstream test drivers
In Python 3.14+, annotations are evaluated lazily per PEP 649/749. When inspect.signature() is called, it tries to resolve annotations by default, which fails if the annotation references an undefined type. Add a version-gated _signature helper that uses annotation_format=annotationlib.Format.STRING on Python 3.14+ to prevent annotation resolution errors. Fixes pytest-dev#629 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Use code object attributes (co_varnames, co_argcount) and __defaults__ directly instead of inspect.signature(). This avoids annotation resolution entirely, which is simpler and more efficient. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Include both the current implementation and the legacy inspect.signature-based version to clearly demonstrate the ~7-66x speedup from using code objects directly. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
A module-level function assigned to a class attribute becomes a bound method on instances, but its __qualname__ has no dot. Track whether the original callable was a bound method before unwrapping to __func__, so self is always stripped for bound methods. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Replace exec()-based test with a direct function definition. The string annotation "NonExistentType" triggers the same behavior without the indirection. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
…lusion Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com> Made-with: Cursor
Move the missing-self warning into varnames where the ambiguity actually lives, instead of bolting it onto HookSpec.__init__. Add a legacy_noself parameter to varnames: when True and the function looks like a class method but lacks self/cls as its first parameter, emit a FutureWarning. HookSpec.__init__ passes legacy_noself=True for class-based non-static hookspecs to support the legacy pattern while warning about it. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
This comment was marked as low quality.
This comment was marked as low quality.
There was a problem hiding this comment.
Hey - I've found 1 security issue, and 2 other issues
Security issues:
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="testing/test_warnings.py" line_range="53-62" />
<code_context>
assert Path(wc.list[0].filename).name == "test_warnings.py"
+
+
+def test_hookspec_missing_self_warns(pm: PluginManager) -> None:
+ """A hookspec defined as a method without ``self`` emits a FutureWarning."""
+
+ class Api:
+ @hookspec
+ def my_hook(item, extra):
+ pass
+
+ with pytest.warns(
+ FutureWarning,
+ match=r"is a method but its first parameter 'item' is not 'self'",
+ ):
+ pm.add_hookspecs(Api)
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert that hookspec registration still produces the expected hook argument names when the warning is emitted
This currently only checks that `pm.add_hookspecs(Api)` emits the `FutureWarning`. To also verify compatibility of `legacy_noself` with hook registration, please assert the resulting hook signature, e.g.:
```python
hook = pm.hook.my_hook
assert hook.spec is not None
assert hook.spec.argnames == ("item", "extra")
```
This ensures the warning path doesn’t alter or break the hook’s argument handling.
</issue_to_address>
### Comment 2
<location path="src/pluggy/_hooks.py" line_range="293" />
<code_context>
+_IMPLICIT_NAMES = ("self", "cls", "obj") if _PYPY else ("self", "cls")
-def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]:
- """Return tuple of positional and keywrord argument names for a function,
- method, class or callable.
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the callable normalization, argument splitting, and implicit-first-arg stripping from `varnames` into small helpers so the main function reads as a simple orchestration of clear steps.
You can keep all of the new behavior but reduce the cognitive load in `varnames` by extracting a few focused helpers. That lets you centralize the “what is this callable / is it a method / what are the args” logic instead of interleaving it.
### 1. Normalize callable + bound detection
Pull out the class / callable-object / unwrap / `ismethod` handling into a helper:
```python
def _normalize_callable(func: object) -> tuple[types.FunctionType, bool]:
is_bound = False
if inspect.isclass(func):
try:
func = func.__init__
except AttributeError: # pragma: no cover - pypy special case
return (), (), False # sentinel; see usage below
is_bound = True
elif not inspect.isroutine(func):
try:
func = getattr(func, "__call__", func)
except Exception: # pragma: no cover - pypy special case
return (), (), False
if inspect.ismethod(func):
is_bound = True
func = inspect.unwrap(func) # type: ignore[arg-type]
if inspect.ismethod(func):
is_bound = True
func = func.__func__
return func, is_bound
```
Then `varnames` starts with:
```python
def varnames(func: object, *, legacy_noself: bool = False) -> tuple[tuple[str, ...], tuple[str, ...]]:
func, is_bound = _normalize_callable(func)
if not isinstance(func, types.FunctionType):
return (), ()
# continue with code/defaults handling...
```
(You can keep your existing early-return sentinels; the main point is isolating this logic.)
### 2. Split positional vs kw-defaults
Hide the `__code__` / `__defaults__` details:
```python
def _split_positional_and_kwdefaults(
func: types.FunctionType,
) -> tuple[tuple[str, ...], tuple[str, ...], str]:
try:
code: types.CodeType = func.__code__ # type: ignore[attr-defined]
defaults: tuple[object, ...] | None = func.__defaults__ # type: ignore[attr-defined]
qualname: str = func.__qualname__ # type: ignore[attr-defined]
except AttributeError: # pragma: no cover
return (), (), ""
args: tuple[str, ...] = code.co_varnames[: code.co_argcount]
if defaults:
index = -len(defaults)
return args[:index], args[index:], qualname
return args, (), qualname
```
Then in `varnames`:
```python
args, kwargs, qualname = _split_positional_and_kwdefaults(func)
if not qualname:
return (), ()
```
### 3. Encapsulate “looks like class method” logic
Move the `__qualname__` parsing and implicit-name decision into one place:
```python
def _looks_like_class_method(qualname: str) -> bool:
tail = qualname.rsplit("<locals>.", maxsplit=1)[-1]
return "." in tail
def _strip_implicit_first_arg(
args: tuple[str, ...],
*,
is_bound: bool,
qualname: str,
legacy_noself: bool,
) -> tuple[str, ...]:
if not args:
return args
if is_bound:
return args[1:]
if not _looks_like_class_method(qualname):
return args
if args[0] in _IMPLICIT_NAMES:
return args[1:]
if legacy_noself:
warnings.warn(
f"{qualname} is a method but its first parameter {args[0]!r} is not 'self'."
" Add 'self' as the first parameter or use @staticmethod."
" This will become an error in a future version of pluggy.",
FutureWarning,
stacklevel=2,
)
return args
```
Then `varnames` becomes:
```python
def varnames(func: object, *, legacy_noself: bool = False) -> tuple[tuple[str, ...], tuple[str, ...]]:
func, is_bound = _normalize_callable(func)
if not isinstance(func, types.FunctionType):
return (), ()
args, kwargs, qualname = _split_positional_and_kwdefaults(func)
if not qualname:
return (), ()
args = _strip_implicit_first_arg(
args, is_bound=is_bound, qualname=qualname, legacy_noself=legacy_noself
)
return args, kwargs
```
This keeps all the behavior you added (PyPy support, structural method detection, `legacy_noself` warning) but centralizes each concern in a small helper with a clear contract. The main `varnames` body becomes a straightforward orchestration of those steps, which should address the “intertwined concerns” and “spread across multiple checks” feedback without changing semantics.
</issue_to_address>
### Comment 3
<location path="downstream/run_downstream.py" line_range="145-150" />
<code_context>
result = subprocess.run(
list(argv),
cwd=cwd,
env=merged_env,
check=False,
)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| def test_hookspec_missing_self_warns(pm: PluginManager) -> None: | ||
| """A hookspec defined as a method without ``self`` emits a FutureWarning.""" | ||
|
|
||
| class Api: | ||
| @hookspec | ||
| def my_hook(item, extra): | ||
| pass | ||
|
|
||
| with pytest.warns( | ||
| FutureWarning, |
There was a problem hiding this comment.
suggestion (testing): Also assert that hookspec registration still produces the expected hook argument names when the warning is emitted
This currently only checks that pm.add_hookspecs(Api) emits the FutureWarning. To also verify compatibility of legacy_noself with hook registration, please assert the resulting hook signature, e.g.:
hook = pm.hook.my_hook
assert hook.spec is not None
assert hook.spec.argnames == ("item", "extra")This ensures the warning path doesn’t alter or break the hook’s argument handling.
| _IMPLICIT_NAMES = ("self", "cls", "obj") if _PYPY else ("self", "cls") | ||
|
|
||
|
|
||
| def varnames(func: object) -> tuple[tuple[str, ...], tuple[str, ...]]: |
There was a problem hiding this comment.
issue (complexity): Consider extracting the callable normalization, argument splitting, and implicit-first-arg stripping from varnames into small helpers so the main function reads as a simple orchestration of clear steps.
You can keep all of the new behavior but reduce the cognitive load in varnames by extracting a few focused helpers. That lets you centralize the “what is this callable / is it a method / what are the args” logic instead of interleaving it.
1. Normalize callable + bound detection
Pull out the class / callable-object / unwrap / ismethod handling into a helper:
def _normalize_callable(func: object) -> tuple[types.FunctionType, bool]:
is_bound = False
if inspect.isclass(func):
try:
func = func.__init__
except AttributeError: # pragma: no cover - pypy special case
return (), (), False # sentinel; see usage below
is_bound = True
elif not inspect.isroutine(func):
try:
func = getattr(func, "__call__", func)
except Exception: # pragma: no cover - pypy special case
return (), (), False
if inspect.ismethod(func):
is_bound = True
func = inspect.unwrap(func) # type: ignore[arg-type]
if inspect.ismethod(func):
is_bound = True
func = func.__func__
return func, is_boundThen varnames starts with:
def varnames(func: object, *, legacy_noself: bool = False) -> tuple[tuple[str, ...], tuple[str, ...]]:
func, is_bound = _normalize_callable(func)
if not isinstance(func, types.FunctionType):
return (), ()
# continue with code/defaults handling...(You can keep your existing early-return sentinels; the main point is isolating this logic.)
2. Split positional vs kw-defaults
Hide the __code__ / __defaults__ details:
def _split_positional_and_kwdefaults(
func: types.FunctionType,
) -> tuple[tuple[str, ...], tuple[str, ...], str]:
try:
code: types.CodeType = func.__code__ # type: ignore[attr-defined]
defaults: tuple[object, ...] | None = func.__defaults__ # type: ignore[attr-defined]
qualname: str = func.__qualname__ # type: ignore[attr-defined]
except AttributeError: # pragma: no cover
return (), (), ""
args: tuple[str, ...] = code.co_varnames[: code.co_argcount]
if defaults:
index = -len(defaults)
return args[:index], args[index:], qualname
return args, (), qualnameThen in varnames:
args, kwargs, qualname = _split_positional_and_kwdefaults(func)
if not qualname:
return (), ()3. Encapsulate “looks like class method” logic
Move the __qualname__ parsing and implicit-name decision into one place:
def _looks_like_class_method(qualname: str) -> bool:
tail = qualname.rsplit("<locals>.", maxsplit=1)[-1]
return "." in tail
def _strip_implicit_first_arg(
args: tuple[str, ...],
*,
is_bound: bool,
qualname: str,
legacy_noself: bool,
) -> tuple[str, ...]:
if not args:
return args
if is_bound:
return args[1:]
if not _looks_like_class_method(qualname):
return args
if args[0] in _IMPLICIT_NAMES:
return args[1:]
if legacy_noself:
warnings.warn(
f"{qualname} is a method but its first parameter {args[0]!r} is not 'self'."
" Add 'self' as the first parameter or use @staticmethod."
" This will become an error in a future version of pluggy.",
FutureWarning,
stacklevel=2,
)
return argsThen varnames becomes:
def varnames(func: object, *, legacy_noself: bool = False) -> tuple[tuple[str, ...], tuple[str, ...]]:
func, is_bound = _normalize_callable(func)
if not isinstance(func, types.FunctionType):
return (), ()
args, kwargs, qualname = _split_positional_and_kwdefaults(func)
if not qualname:
return (), ()
args = _strip_implicit_first_arg(
args, is_bound=is_bound, qualname=qualname, legacy_noself=legacy_noself
)
return args, kwargsThis keeps all the behavior you added (PyPy support, structural method detection, legacy_noself warning) but centralizes each concern in a small helper with a clear contract. The main varnames body becomes a straightforward orchestration of those steps, which should address the “intertwined concerns” and “spread across multiple checks” feedback without changing semantics.
| result = subprocess.run( | ||
| list(argv), | ||
| cwd=cwd, | ||
| env=merged_env, | ||
| check=False, | ||
| ) |
There was a problem hiding this comment.
security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
Source: opengrep
There was a problem hiding this comment.
Pull request overview
This PR has two distinct purposes bundled together: (1) the main upstream fix — reimplementing varnames() in pluggy._hooks to read positional argument names directly from __code__/__defaults__/__qualname__ instead of inspect.signature(), so that hookspec/hookimpl registration no longer fails on Python 3.14+ when annotations contain unresolvable forward references; and (2) a substantial overhaul of the downstream/ testing infrastructure, replacing per-project bash scripts with a single PEP 723 driver that consumes TOML "recipes", plus a new GitHub Actions workflow that runs those recipes.
Changes:
- Rewrite
varnames()to be annotation-resolution-free, add a newlegacy_noselfflag with aFutureWarningfor hookspec classes whose methods omitself, and add_IMPLICIT_NAMES(now including"cls"). - Replace
downstream/*.shscripts with TOML recipes plus arun_downstream.pydriver (pydantic-validated), add a.github/workflows/downstream.yml, and document the new layout indownstream/README.mdandRELEASING.rst. - Add tests covering the new
varnamesbehavior, a_varnames_legacybenchmark for comparison, a doc clarification about hookimpl defaults, and small documentation/spelling fixes plus three changelog entries.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pluggy/_hooks.py | Core varnames() rewrite; _IMPLICIT_NAMES adds cls; HookSpec.__init__ opts into legacy_noself for non-staticmethod class members. |
| testing/test_helpers.py | New unit tests for varnames across bound/unbound/classmethod/staticmethod/no-self/unresolvable-annotation cases. |
| testing/test_warnings.py | New tests asserting FutureWarning (or absence) for hookspec methods without self. |
| testing/test_hookcaller.py | Adds filterwarnings("ignore::FutureWarning") to a test that triggers the new warning. |
| testing/benchmark.py | Adds a _varnames_legacy reference impl and parametrized benchmarks comparing new vs old. |
| downstream/run_downstream.py | New pydantic+tomllib driver that clones a repo and runs a recipe's install/test steps under a uv venv (or delegates to a bash script). |
| downstream/recipes/*.toml, conda.bash | New TOML recipes plus a conda bootstrap script replacing the old per-project shell scripts. |
| downstream/*.sh (removed) | Deleted in favor of recipes. |
| downstream/README.md, RELEASING.rst | Document the new recipe-based workflow and fix a typo. |
| downstream/.gitignore | Ignore the new python-lsp-server/ clone directory. |
| .github/workflows/downstream.yml | New CI workflow that runs each recipe in a matrix on PRs whose branch name contains downstream or via manual dispatch. |
| docs/index.rst | Convert two broken intersphinx links to direct URLs (tox, Kedro) and document that hookimpl args with defaults are not passed. |
| changelog/186.doc.rst, 522.doc.rst, 629.bugfix.rst | Changelog entries for the doc fixes and the 3.14 hooks-registration fix. |
| .pre-commit-config.yaml | Bump mypy mirror from v1.20.2 to v2.0.0. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| _tail = qualname.rsplit("<locals>.", maxsplit=1)[-1] | ||
| _is_class_method = "." in _tail | ||
| if args: | ||
| qualname: str = getattr(func, "__qualname__", "") | ||
| if inspect.ismethod(func) or ("." in qualname and args[0] in implicit_names): | ||
| if is_bound: | ||
| args = args[1:] | ||
| elif _is_class_method and args[0] in _IMPLICIT_NAMES: | ||
| args = args[1:] | ||
| elif _is_class_method and legacy_noself: |
| env = {**os.environ, **(extra or {})} | ||
| # Empty-string values mean "remove from environment". | ||
| env = {k: v for k, v in env.items() if v != ""} |
| _PYPY = sys.implementation.name == "pypy" | ||
| _IMPLICIT_NAMES = ("self", "cls", "obj") if _PYPY else ("self", "cls") |
Trigger downstream tests for the varnames/py3.14 fix branch.
Upstream PR: pytest-dev#632
Made with Cursor
Summary by Sourcery
Refine varnames handling for methods and annotations, add compatibility warnings for legacy hookspecs, and introduce a TOML/uv-based downstream testing framework with CI integration.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Tests: