You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AI-authored. I prompted this exploration; the agent (Claude Opus 5 via Claude Code)
wrote the analysis and this text. I reviewed it and I am posting it.
Follow-up to #15035, which makes pytest record why it deselected an item and show it
at -v.
The reason there is side-channeled: _pytest.deselect.deselect_items() stashes it on
the config for the duration of the pytest_deselected call, and pytest's terminal
reporter reads it back. It is private, so a plugin can neither record a reason for its
own deselections nor read the reason for pytest's.
That is the whole point of this issue: the side channel should not become the API.
Why the hook cannot just grow the argument
pytest_deselected is unusual — plugins call it, not only implement it. Calling it
from pytest_collection_modifyitems when you remove items is part of the documented
contract. So reason would be a hookspec argument that:
no existing caller passes, so implementations can never rely on it;
can never become required, because requiring it breaks every plugin that supports
more than one pytest version;
cannot be told apart, when absent, from "the caller had nothing to say".
pluggy has no way to fill in an argument on behalf of an old caller. This is pytest-dev/pluggy#170 ("Howto handle hook changes"), where this very hook is named as
the motivating case, and pytest-dev/pluggy#361 proposes the mechanism that would
resolve it: letting a hookspec take part in the call and normalize its arguments.
pytest_deselected gains reason: str | None, with the spec supplying None for
callers that do not pass it.
_pytest/deselect.py becomes a thin shim, or goes away; the terminal reporter reads
the hook argument instead of the stash.
The placeholder pytest currently prints for plugin deselections
(deselected by a plugin, no reason recorded) stops being the normal case.
Alternatives worth weighing before that
A new hook (pytest_items_deselected(items, reason)) alongside the old one, with
the old one kept for compatibility. Runs into the "both implementations fire" problem
from Howto handle hook changes pluggy#170 and needs a deprecation story anyway.
A deselection API on Session/Config that plugins call instead of the hook
(session.deselect(items, reason=...)), with the hook fired internally. This was on
the table in 2016; doing it properly means retiring pytest_collection_modifyitems
as the place where selection is decided, which is a much bigger change — see the
discussion in logging: pytest should allow users to know why a test was deselected #13617.
Making the side channel public is not on that list on purpose: it would be a second,
parallel way to pass an argument to a hook, and it would have to be kept alive
regardless of what pluggy ends up offering.
Follow-up to #15035, which makes pytest record why it deselected an item and show it
at
-v.The reason there is side-channeled:
_pytest.deselect.deselect_items()stashes it onthe config for the duration of the
pytest_deselectedcall, and pytest's terminalreporter reads it back. It is private, so a plugin can neither record a reason for its
own deselections nor read the reason for pytest's.
That is the whole point of this issue: the side channel should not become the API.
Why the hook cannot just grow the argument
pytest_deselectedis unusual — plugins call it, not only implement it. Calling itfrom
pytest_collection_modifyitemswhen you remove items is part of the documentedcontract. So
reasonwould be a hookspec argument that:more than one pytest version;
pluggy has no way to fill in an argument on behalf of an old caller. This is
pytest-dev/pluggy#170 ("Howto handle hook changes"), where this very hook is named as
the motivating case, and pytest-dev/pluggy#361 proposes the mechanism that would
resolve it: letting a hookspec take part in the call and normalize its arguments.
What would need to happen
express "this argument is new; here is what old callers mean".
pytest_deselectedgainsreason: str | None, with the spec supplyingNoneforcallers that do not pass it.
_pytest/deselect.pybecomes a thin shim, or goes away; the terminal reporter readsthe hook argument instead of the stash.
(
deselected by a plugin, no reason recorded) stops being the normal case.Alternatives worth weighing before that
pytest_items_deselected(items, reason)) alongside the old one, withthe old one kept for compatibility. Runs into the "both implementations fire" problem
from Howto handle hook changes pluggy#170 and needs a deprecation story anyway.
Session/Configthat plugins call instead of the hook(
session.deselect(items, reason=...)), with the hook fired internally. This was onthe table in 2016; doing it properly means retiring
pytest_collection_modifyitemsas the place where selection is decided, which is a much bigger change — see the
discussion in logging: pytest should allow users to know why a test was deselected #13617.
Making the side channel public is not on that list on purpose: it would be a second,
parallel way to pass an argument to a hook, and it would have to be kept alive
regardless of what pluggy ends up offering.