diff --git a/AUTHORS b/AUTHORS index 347efad57..46d161a72 100644 --- a/AUTHORS +++ b/AUTHORS @@ -271,6 +271,7 @@ Matt Bachmann Matt Duck Matt Williams Matthias Hafner +Max Berkowitz Maxim Filipenko Maximilian Cosmo Sitter mbyt diff --git a/changelog/10514.improvement.rst b/changelog/10514.improvement.rst new file mode 100644 index 000000000..12430ba26 --- /dev/null +++ b/changelog/10514.improvement.rst @@ -0,0 +1 @@ +Emit a warning when unregistered marks are used with the command line option -m. diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 950a6959e..31683a0d4 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -236,7 +236,7 @@ def deselect_by_mark(items: "List[Item]", config: Config) -> None: if not matchexpr: return - expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'") + expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'", True) remaining: List[Item] = [] deselected: List[Item] = [] for item in items: @@ -249,9 +249,9 @@ def deselect_by_mark(items: "List[Item]", config: Config) -> None: items[:] = remaining -def _parse_expression(expr: str, exc_message: str) -> Expression: +def _parse_expression(expr: str, exc_message: str, mark: bool = False) -> Expression: try: - return Expression.compile(expr) + return Expression.compile(expr, mark) except ParseError as e: raise UsageError(f"{exc_message}: {expr}: {e}") from None diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 78b7fda69..52ec2bca1 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -27,6 +27,8 @@ from typing import NoReturn from typing import Optional from typing import Sequence +from .structures import MARK_GEN + __all__ = [ "Expression", @@ -197,12 +199,19 @@ class Expression: self.code = code @classmethod - def compile(self, input: str) -> "Expression": + def compile(self, input: str, mark: bool = False) -> "Expression": """Compile a match expression. :param input: The input expression - one line. """ astexpr = expression(Scanner(input)) + + if mark: + for node in ast.walk(astexpr): + # if the node is an identifier, i.e. a mark name + if isinstance(node, ast.Name): + MARK_GEN.verify_mark(node.id[len(IDENT_PREFIX) :]) + code: types.CodeType = compile( astexpr, filename="", diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 456808063..73c72476b 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -520,11 +520,7 @@ class MarkGenerator: self._config: Optional[Config] = None self._markers: Set[str] = set() - def __getattr__(self, name: str) -> MarkDecorator: - """Generate a new :class:`MarkDecorator` with the given name.""" - if name[0] == "_": - raise AttributeError("Marker name must NOT start with underscore") - + def verify_mark(self, name: str) -> None: if self._config is not None: # We store a set of markers as a performance optimisation - if a mark # name is in the set we definitely know it, but a mark may be known and @@ -556,9 +552,16 @@ class MarkGenerator: "custom marks to avoid this warning - for details, see " "https://docs.pytest.org/en/stable/how-to/mark.html", PytestUnknownMarkWarning, - 2, + 3, ) + def __getattr__(self, name: str) -> MarkDecorator: + """Generate a new :class:`MarkDecorator` with the given name.""" + if name[0] == "_": + raise AttributeError("Marker name must NOT start with underscore") + + self.verify_mark(name) + return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)