-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Clarify the relation between marks and keywords #15011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c55b2ae
ef4c6a9
172d896
5837e07
b04fff5
5cd719b
b2e6948
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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``. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| :meth:`Node.add_marker <_pytest.nodes.Node.add_marker>` now stores a :class:`~pytest.Mark` in ``node.keywords``, matching what marks applied during collection store. Previously it stored a :class:`~pytest.MarkDecorator`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,15 +162,24 @@ Or select multiple nodes: | |
| when running pytest with the ``-rf`` option. You can also | ||
| construct Node IDs from the output of ``pytest --collect-only``. | ||
|
|
||
| Using ``-k expr`` to select tests based on their name | ||
| Using ``-k expr`` to select tests by keyword | ||
| ------------------------------------------------------- | ||
|
|
||
| .. versionadded:: 2.0/2.3.4 | ||
|
|
||
| You can use the :option:`-k` command line option to specify an expression | ||
| which implements a substring match on the test names instead of the | ||
| exact match on markers that :option:`-m` provides. This makes it easy to | ||
| select tests based on their names: | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would change this to a listing, seems easier to parse/make sense of it, and include examples: |
||
| (usually the name of the file and class it is in), the names of the markers | ||
| applied to it or to its parents, attributes set on the test function, and any | ||
| :attr:`extra keywords <_pytest.nodes.Node.extra_keyword_matches>` explicitly | ||
| added to it or to its parents. | ||
|
|
||
| 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. | ||
|
Comment on lines
+180
to
+182
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This paragraph is a little confusing since the example doesn't use I think either omit the paragraph or integrate it into the example. |
||
|
|
||
| .. versionchanged:: 5.4 | ||
|
|
||
|
|
@@ -225,11 +234,6 @@ Or to select "http" and "quick" tests: | |
| You can use ``and``, ``or``, ``not`` and parentheses. | ||
|
|
||
|
|
||
| In addition to the test's name, :option:`-k` also matches the names of the test's parents (usually, the name of the file and class it's in), | ||
| attributes set on the test function, markers applied to it or its parents and any :attr:`extra keywords <_pytest.nodes.Node.extra_keyword_matches>` | ||
| explicitly added to it or its parents. | ||
|
|
||
|
|
||
| Registering markers | ||
| ------------------------------------- | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,11 +38,16 @@ Pytest supports several ways to run and select tests from the command-line or fr | |
|
|
||
| pytest -k 'MyClass and not method' | ||
|
|
||
| This will run tests which contain names that match the given *string expression* (case-insensitive), | ||
| which can include Python operators that use filenames, class names and function names as variables. | ||
| This will run tests whose *keywords* match the given expression (case-insensitive). | ||
| 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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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? |
||
| the names of the markers applied to it or to its parents, and attributes assigned directly to the | ||
| test function. Each name in the expression is matched as a *substring* of any of them, so | ||
| ``-k slow`` selects both tests marked ``@pytest.mark.slow`` and tests merely named | ||
| ``test_slow_path``. Use :option:`-m` to match markers and nothing else. | ||
|
|
||
| .. _nodeids: | ||
|
|
||
| **Run tests by collection arguments** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2870,17 +2870,27 @@ Test Selection | |
|
|
||
| .. option:: -k EXPRESSION | ||
|
|
||
| Only run tests which match the given substring expression. | ||
| An expression is a Python evaluable expression where all names are substring-matched against test names and their parent classes. | ||
| Only run tests which match the given keyword expression. | ||
| An expression is made of names combined with ``and``, ``or``, ``not`` and parentheses. | ||
| Each name is matched case-insensitively as a substring of any of the test's keywords. | ||
|
|
||
| Examples:: | ||
|
|
||
| pytest -k "test_method or test_other" # matches names containing 'test_method' OR 'test_other' | ||
| pytest -k "not test_method" # matches names NOT containing 'test_method' | ||
| pytest -k "test_method or test_other" # matches keywords containing 'test_method' OR 'test_other' | ||
| pytest -k "not test_method" # matches keywords NOT containing 'test_method' | ||
| pytest -k "not test_method and not test_other" # excludes both | ||
|
|
||
| The matching is case-insensitive. | ||
| Keywords are also matched to classes and functions containing extra names in their ``extra_keyword_matches`` set. | ||
| The keywords of a test are: | ||
|
|
||
| * its own name, including any parametrization id; | ||
| * the names of its parent class, module and directories; | ||
| * the names of the markers applied to it or to any of its parents; | ||
| * attributes assigned directly to the test function, as in the legacy ``test_func.slow = True`` style; | ||
| * any names added to the :attr:`~_pytest.nodes.Node.extra_keyword_matches` set of it or of a parent. | ||
|
|
||
| Because marker names are keywords, ``-k slow`` selects both tests marked ``@pytest.mark.slow`` | ||
| and tests whose name merely contains ``slow``. | ||
| Use :option:`-m` to match markers and nothing else. | ||
|
Comment on lines
+2891
to
+2893
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would remove this paragraph from the reference documentation. |
||
|
|
||
| See :ref:`select-tests` for more information and examples. | ||
|
|
||
|
|
@@ -2895,6 +2905,10 @@ Test Selection | |
| pytest -m "not slow" # run tests NOT marked slow | ||
| pytest -m "mark1 and not mark2" # run tests marked mark1 but not mark2 | ||
|
|
||
| Marker names are matched exactly and case-sensitively, and only markers are matched: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would remove reference to |
||
| 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')"``. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would move the example to a line in |
||
|
|
||
| See :ref:`mark` for more information on markers. | ||
|
|
||
| .. option:: --markers | ||
|
|
@@ -3441,21 +3455,21 @@ All the command-line flags can also be obtained by running ``pytest --help``:: | |
| file_or_dir | ||
|
|
||
| general: | ||
| -k EXPRESSION Only run tests which match the given substring | ||
| expression. An expression is a Python evaluable | ||
| expression where all names are substring-matched | ||
| against test names and their parent classes. | ||
| Example: -k 'test_method or test_other' matches all | ||
| test functions and classes whose name contains | ||
| -k EXPRESSION Only run tests which match the given keyword | ||
| expression. An expression is made of names combined | ||
| with 'and', 'or', 'not' and parentheses; each name | ||
| is matched case-insensitively as a substring of any | ||
| of the test's keywords. Example: -k 'test_method or | ||
| test_other' matches all tests whose keywords contain | ||
| 'test_method' or 'test_other', while -k 'not | ||
| test_method' matches those that don't contain | ||
| 'test_method' in their names. -k 'not test_method | ||
| and not test_other' will eliminate the matches. | ||
| Additionally keywords are matched to classes and | ||
| functions containing extra names in their | ||
| 'extra_keyword_matches' set, as well as functions | ||
| which have names assigned directly to them. The | ||
| matching is case-insensitive. | ||
| test_method' matches those that do not. The keywords | ||
| of a test are its own name including any | ||
| parametrization id, the names of its parent class, | ||
| module and directories, the names of the markers | ||
| applied to it or to its parents, attributes assigned | ||
| directly to the test function, and any names in an | ||
| 'extra_keyword_matches' set. Unlike -m, -k matches | ||
| substrings and cannot match marker arguments. | ||
| -m MARKEXPR Only run tests matching given mark expression. For | ||
| example: -m 'mark1 and not mark2'. | ||
| --markers show markers (builtin, plugin and per-project ones). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -618,7 +618,7 @@ def path(self) -> Path: | |
|
|
||
| @property | ||
| def keywords(self) -> MutableMapping[str, Any]: | ||
| """Keywords/markers dictionary for the underlying node.""" | ||
| """The :attr:`~_pytest.nodes.Node.keywords` of the underlying node.""" | ||
| node: nodes.Node = self.node | ||
| return node.keywords | ||
|
|
||
|
|
@@ -636,8 +636,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: | |
| def applymarker(self, marker: str | MarkDecorator) -> None: | ||
| """Apply a marker to a single test function invocation. | ||
|
|
||
| This method is useful if you don't want to have a keyword/marker | ||
| on all function invocations. | ||
| This method is useful if you don't want to have the marker on all | ||
| function invocations. | ||
|
Comment on lines
+639
to
+640
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (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? |
||
|
|
||
| :param marker: | ||
| An object created by a call to ``pytest.mark.NAME(...)``. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,18 +94,18 @@ def pytest_addoption(parser: Parser) -> None: | |
| dest="keyword", | ||
| default="", | ||
| metavar="EXPRESSION", | ||
| help="Only run tests which match the given substring expression. " | ||
| "An expression is a Python evaluable expression " | ||
| "where all names are substring-matched against test names " | ||
| "and their parent classes. Example: -k 'test_method or test_" | ||
| "other' matches all test functions and classes whose name " | ||
| "contains 'test_method' or 'test_other', while -k 'not test_method' " | ||
| "matches those that don't contain 'test_method' in their names. " | ||
| "-k 'not test_method and not test_other' will eliminate the matches. " | ||
| "Additionally keywords are matched to classes and functions " | ||
| "containing extra names in their 'extra_keyword_matches' set, " | ||
| "as well as functions which have names assigned directly to them. " | ||
| "The matching is case-insensitive.", | ||
| help="Only run tests which match the given keyword expression. " | ||
| "An expression is made of names combined with 'and', 'or', 'not' " | ||
| "and parentheses; each name is matched case-insensitively as a " | ||
| "substring of any of the test's keywords. Example: -k 'test_method " | ||
| "or test_other' matches all tests whose keywords contain " | ||
| "'test_method' or 'test_other', while -k 'not test_method' matches " | ||
| "those that do not. The keywords of a test are its own name " | ||
| "including any parametrization id, the names of its parent class, " | ||
| "module and directories, the names of the markers applied to it or " | ||
| "to its parents, attributes assigned directly to the test function, " | ||
| "and any names in an 'extra_keyword_matches' set. Unlike -m, -k " | ||
| "matches substrings and cannot match marker arguments.", | ||
| ) | ||
|
|
||
| group._addoption( # private to use reserved lower-case short option | ||
|
|
@@ -150,19 +150,33 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None: | |
| return None | ||
|
|
||
|
|
||
| #: Attributes which are never meaningful as keywords, but do end up in the | ||
| #: ``__dict__`` of a test function: pytest's own mark storage, and the | ||
| #: bookkeeping decorators leave behind (``functools.wraps`` copies ``__wrapped__`` | ||
| #: and friends, ``functools.lru_cache`` adds ``cache_parameters``, ...). | ||
| IGNORED_FUNCTION_ATTRIBUTES = frozenset({"pytestmark", "cache_parameters"}) | ||
|
|
||
|
|
||
| def _is_matchable_function_attribute(name: str) -> bool: | ||
| """Whether a test function attribute may be matched by ``-k``.""" | ||
| return not name.startswith("_") and name not in IGNORED_FUNCTION_ATTRIBUTES | ||
|
|
||
|
|
||
| @dataclasses.dataclass | ||
| class KeywordMatcher: | ||
| """A matcher for keywords. | ||
| """A matcher for keywords, used by ``-k``. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO the low level Would at least change to "as used by |
||
|
|
||
| Given a list of names, matches any substring of one of these names. The | ||
| Given a set of names, matches any substring of one of these names. The | ||
| string inclusion check is case-insensitive. | ||
|
|
||
| Will match on the name of colitem, including the names of its parents. | ||
| Only matches names of items which are either a :class:`Class` or a | ||
| :class:`Function`. | ||
|
Comment on lines
-161
to
-162
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems this sentence was incorrect leftover from 60906f7. |
||
| The names are collected in :meth:`from_item` from the item and its | ||
| parents: their node names, the names of the markers in scope, the | ||
| attributes assigned to the test function, and the | ||
| :attr:`~_pytest.nodes.Node.extra_keyword_matches` sets. | ||
|
|
||
| Additionally, matches on names in the 'extra_keyword_matches' set of | ||
| any item, as well as names directly assigned to test functions. | ||
| Note that these names are collected independently of | ||
| :attr:`Node.keywords <_pytest.nodes.Node.keywords>`; writing into that | ||
| mapping does not affect ``-k``. | ||
| """ | ||
|
|
||
| __slots__ = ("_names",) | ||
|
|
@@ -190,10 +204,16 @@ def from_item(cls, item: Item) -> KeywordMatcher: | |
| # Add the names added as extra keywords to current or parent items. | ||
| mapped_names.update(item.listextrakeywords()) | ||
|
|
||
| # Add the names attached to the current function through direct assignment. | ||
| # Add the names attached to the current function through direct | ||
| # assignment, ignoring the attributes that merely happen to live in the | ||
| # function's __dict__ without anyone meaning them as keywords. | ||
| function_obj = getattr(item, "function", None) | ||
| if function_obj: | ||
| mapped_names.update(function_obj.__dict__) | ||
| mapped_names.update( | ||
| name | ||
| for name in function_obj.__dict__ | ||
| if _is_matchable_function_attribute(name) | ||
| ) | ||
|
|
||
| # Add the markers to the keywords as we no longer handle them correctly. | ||
| mapped_names.update(mark.name for mark in item.iter_markers()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -370,8 +370,9 @@ def __init__( | |
| #: The line number is 0-based. | ||
| self.location: tuple[str, int | None, str] = location | ||
|
|
||
| #: 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we document the |
||
| #: are not carried over. | ||
| self.keywords: Mapping[str, Any] = keywords | ||
|
|
||
| #: Test outcome, always one of "passed", "failed", "skipped". | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.