Clarify the relation between marks and keywords - #15011
Clarify the relation between marks and keywords#15011RonnyPfannschmidt wants to merge 7 commits into
Conversation
The -k matcher scans a test function's __dict__ to support the legacy `test_fn.foo = True` keyword idiom, but that scan also picked up attributes nobody meant as keywords: pytest's own `pytestmark` storage, and the bookkeeping decorators leave behind (`__wrapped__` and the rest of what functools.wraps copies, lru_cache's `cache_parameters`). So `-k wrapped` selected every wraps-decorated test and `-k pytestmark` selected every directly marked one. Skip private and dunder names, plus a named set of known attributes. Part of pytest-dev#4569. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
Marks applied during collection are stored in node.keywords as Mark objects; add_marker stored the MarkDecorator instead, so the value type depended on how the mark got there. Nothing inside pytest reads keyword values, and both types expose name/args/kwargs, so downstream `item.keywords["x"].args` keeps working. Part of pytest-dev#4569. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The -k help text still described the pre-5.4 implementation ("a Python
evaluable expression"), claimed matching is limited to test names and
their parent classes, and never mentioned that marker names are
keywords - which is the behaviour people trip over in pytest-dev#4569.
Replace it with the real list of keyword sources, and state the
substring/case-insensitive contrast with -m in both option blocks.
Part of pytest-dev#4569.
Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
KeywordMatcher claimed it only matches Class and Function names, which stopped being true in 2.4.0; Function's `keywords` parameter claimed it feeds "-k" matching, which it does not; TestReport.keywords promised a name -> value mapping while the values are all 1. Also state what Node.keywords is for, and that -k does not read it. Part of pytest-dev#4569. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The markers.rst section title and intro presented -k as matching test names, in contrast to -m matching markers, and only corrected itself in a trailing paragraph after three console dumps. usage.rst, the page the option reference links to, still described the pre-5.4 eval semantics and listed only filenames, classes and functions. Lead with what a keyword is in both places, and say plainly that marker names are keywords. Part of pytest-dev#4569. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
The --runslow and incremental examples tested for a marker with `"name" in item.keywords`, which is also true when a parent node happens to be named that, or when the test function carries an attribute of that name. These snippets are widely copied, so they are where the idiom of treating keywords as a mark lookup keeps coming from. Part of pytest-dev#4569. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
Several long-standing behaviours had no test: -k matching marks from the module, class and base classes; -k ignoring marker arguments; -k seeing a mark added by a conftest collection hook; extra_keyword_matches on an item rather than a collector; and writing into item.keywords having no effect on either -k or -m, despite the 2.3.4 changelog claiming it "integrates with the -m option". Also enable the -k half of test_mark_expressions_no_smear, commented out since the marker transfer that made marks smear onto a shared base class was removed, and pin that -m is case sensitive. Part of pytest-dev#4569. Co-Authored-By: Claude Opus 5 (1M context) via Claude Code <noreply@anthropic.com>
Pierre-Sassoulas
left a comment
There was a problem hiding this comment.
Didn't see anything outrageous but I'm not sure I'm knowledgable enough to do this review.
bluetech
left a comment
There was a problem hiding this comment.
Thanks, look like good changes to me.
| Because marker names are keywords, ``-k http`` below selects tests marked | ||
| ``@pytest.mark.http`` just as well as tests merely named ``test_send_http``. | ||
| Use :option:`-m` when you want markers and nothing else. |
There was a problem hiding this comment.
This paragraph is a little confusing since the example doesn't use @pytest.mark.http. I understand you intend to say "if hypothetically a test were marked with @pytest.mark.http then it would also be selected` but it doesn't quite read this way.
I think either omit the paragraph or integrate it into the example.
| which implements a substring match on the test's *keywords*, instead of the | ||
| exact match on markers that :option:`-m` provides. | ||
|
|
||
| The keywords of a test are its own name, the names of the test's parents |
There was a problem hiding this comment.
I figure the reason why this paragraph was previously at the end was a desire to not inundate the reader with details before giving the example. Which I kinda agree with, though both ways are OK.
There was a problem hiding this comment.
I would change this to a listing, seems easier to parse/make sense of it, and include examples:
The keywords of a test are:
* Its own name (e.g. `test_foo`).
* The name of the test's parents (usually the name of the file and class it is in) (e.g. `TestFoo`, `test_foo.py`)
* ...
| @@ -0,0 +1 @@ | |||
| The documentation of :option:`-k` now lists what a test's keywords actually are, including marker names, and says how ``-k`` differs from :option:`-m`. The examples that looked up a marker now use :meth:`Node.get_closest_marker <_pytest.nodes.Node.get_closest_marker>` instead of ``item.keywords``. | |||
There was a problem hiding this comment.
It would be nice to link to said documentation.
Does it refer to example/markers.rst? If so, it did explain what "keywords" are. So I think there first sentence here can be removed.
| @@ -0,0 +1 @@ | |||
| ``-k`` no longer matches attributes that end up in a test function's ``__dict__`` without being meant as keywords, such as pytest's own ``pytestmark`` storage and the bookkeeping left behind by :func:`functools.wraps` and :func:`functools.lru_cache`. | |||
There was a problem hiding this comment.
| ``-k`` no longer matches attributes that end up in a test function's ``__dict__`` without being meant as keywords, such as pytest's own ``pytestmark`` storage and the bookkeeping left behind by :func:`functools.wraps` and :func:`functools.lru_cache`. | |
| :option:`-k` no longer matches attributes that end up in a test function's ``__dict__`` without being meant as keywords, such as pytest's own ``pytestmark`` attribute and the bookkeeping left behind by :func:`functools.wraps` and :func:`functools.lru_cache`. |
| The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``. | ||
| Use ``""`` instead of ``''`` in expression when running this on Windows | ||
|
|
||
| The keywords of a test are its own name, the names of the file, class and directories it is in, |
There was a problem hiding this comment.
usage.rst is one of the "started" documentation pages, so I think it would be nice to keep it relatively short. Maybe can replace this paragraph with a hyperlink to example/markers.rst in the above paragraph?
|
|
||
| Marker names are matched exactly and case-sensitively, and only markers are matched: | ||
| unlike :option:`-k`, ``-m`` never matches test, class, module or directory names. | ||
| Marker keyword arguments can be matched as well, as in ``pytest -m "device(serial='123')"``. |
There was a problem hiding this comment.
Would move the example to a line in Examples above.
| @dataclasses.dataclass | ||
| class KeywordMatcher: | ||
| """A matcher for keywords. | ||
| """A matcher for keywords, used by ``-k``. |
There was a problem hiding this comment.
IMO the low level KeywordMatcher shouldn't refer to high level -k argument. Though maybe it makes things clearer.
Would at least change to "as used by -k".
| Only matches names of items which are either a :class:`Class` or a | ||
| :class:`Function`. |
There was a problem hiding this comment.
Seems this sentence was incorrect leftover from 60906f7.
| This method is useful if you don't want to have the marker on all | ||
| function invocations. |
There was a problem hiding this comment.
(Preexisting) I wonder what "function invocation" refers to here? I can't make sense of what it's trying to say, maybe refers to parametrization?
| #: A name -> value dictionary containing all keywords and | ||
| #: markers associated with a test invocation. | ||
| #: The names in :attr:`Node.keywords <_pytest.nodes.Node.keywords>` | ||
| #: of the item, each mapping to ``1``; the values of the node keywords |
There was a problem hiding this comment.
If we document the 1 then it would be good to change the type annotation to Literal[1] to statically guarantee it.
| which implements a substring match on the test's *keywords*, instead of the | ||
| exact match on markers that :option:`-m` provides. | ||
|
|
||
| The keywords of a test are its own name, the names of the test's parents |
There was a problem hiding this comment.
I would change this to a listing, seems easier to parse/make sense of it, and include examples:
The keywords of a test are:
* Its own name (e.g. `test_foo`).
* The name of the test's parents (usually the name of the file and class it is in) (e.g. `TestFoo`, `test_foo.py`)
* ...
I prompted an agent (Claude Opus 5 in Claude Code) to work through #4569. The
investigation, the commits and this description are its work; I read it and I
am posting it.
#4569 asks to revisit the relation between marks and keywords. Mapping what
that relation actually is came first, and it changes the shape of the issue:
-kdoes not readitem.keywords, and has not since 2.4.0 (#306).KeywordMatcher.from_itembuilds its own name set from the node chain,extra_keyword_matches, the test function's__dict__anditer_markers().node.keywordsis a separate mapping, fed by different code, read only byrequest.keywords,TestReport.keywordsand the terminal's skip folding.So the two mark-to-keyword hacks from #4564 and #4649 are independent of one
another, and the
# todo: this is a hell of a hackcomment pointing at #4569sits on the one that does not influence selection at all. That makes most of
#4569 a documentation and coverage problem rather than a refactoring one.
One commit per item:
Behaviour
-kno longer matches attributes that land in a test function's__dict__without anyone meaning them as keywords.
-k wrappedselected everyfunctools.wraps-decorated test and-k pytestmarkevery directly markedone. Private and dunder names are skipped, plus a named set. The legacy
test_func.slow = Trueidiom keeps working.add_markerstores aMarkinnode.keywordsinstead of aMarkDecorator, matching what collection-time marks store.Documentation
-khelp text described the pre-5.4 implementation ("a Python evaluableexpression"), limited matching to test names and parent classes, and never
mentioned markers — the thing The-Compiler's comment on revisit relation between marks and keywords #4569 asks for. It
now lists the real keyword sources and contrasts with
-m.markers.rstframed-kas name-only and corrected itself only in atrailing paragraph;
usage.rst, which the option reference links to, stilldescribed eval semantics.
KeywordMatcher,Function'skeywordsparameter,TestReport.keywords,Node.keywordsandFixtureRequest.keywordsdocstrings were stale or wrong.--runslowand incremental examples useget_closest_markerrather than"name" in item.keywords.Tests
Pins
-kmatching marks from module/class/base classes,-kignoring markerarguments,
-kseeing a mark added by a conftest collection hook,extra_keyword_matcheson an item, and writing intoitem.keywordshaving noeffect on
-kor-m— despite the 2.3.4 changelog claiming it integrateswith
-m. Also enables the-khalf oftest_mark_expressions_no_smear,commented out since the marker transfer was removed, and pins that
-miscase sensitive.
Deliberately left open
Whether the
function.__dict__scan should exist at all is the remainingdesign question in #4569. Dropping it means deprecating attribute-marks and
finding another discriminator for the skip folding in
terminal.py, whichkeys off
"pytestmark" in report.keywords. UnifyingKeywordMatcherwithnode.keywordsdepends on that answer, so neither is attempted here.🤖 Generated with Claude Code