updates
This commit is contained in:
@@ -1,19 +1,16 @@
|
||||
"""Generic mechanism for marking and selecting python functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Set as AbstractSet
|
||||
import dataclasses
|
||||
from typing import AbstractSet
|
||||
from typing import Collection
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from .expression import Expression
|
||||
from .structures import _HiddenParam
|
||||
from .expression import ParseError
|
||||
from .structures import EMPTY_PARAMETERSET_OPTION
|
||||
from .structures import get_empty_parameterset_mark
|
||||
from .structures import HIDDEN_PARAM
|
||||
from .structures import Mark
|
||||
from .structures import MARK_GEN
|
||||
from .structures import MarkDecorator
|
||||
@@ -23,17 +20,14 @@ from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import NOT_SET
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.stash import StashKey
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.nodes import Item
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HIDDEN_PARAM",
|
||||
"MARK_GEN",
|
||||
"Mark",
|
||||
"MarkDecorator",
|
||||
@@ -43,13 +37,13 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
old_mark_config_key = StashKey[Config | None]()
|
||||
old_mark_config_key = StashKey[Optional[Config]]()
|
||||
|
||||
|
||||
def param(
|
||||
*values: object,
|
||||
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
|
||||
id: str | _HiddenParam | None = None,
|
||||
marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (),
|
||||
id: Optional[str] = None,
|
||||
) -> ParameterSet:
|
||||
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
|
||||
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
|
||||
@@ -67,34 +61,22 @@ def param(
|
||||
assert eval(test_input) == expected
|
||||
|
||||
:param values: Variable args of the values of the parameter set, in order.
|
||||
|
||||
:param marks:
|
||||
A single mark or a list of marks to be applied to this parameter set.
|
||||
|
||||
:ref:`pytest.mark.usefixtures <pytest.mark.usefixtures ref>` cannot be added via this parameter.
|
||||
|
||||
:type id: str | Literal[pytest.HIDDEN_PARAM] | None
|
||||
:param id:
|
||||
The id to attribute to this parameter set.
|
||||
|
||||
.. versionadded:: 8.4
|
||||
:ref:`hidden-param` means to hide the parameter set
|
||||
from the test name. Can only be used at most 1 time, as
|
||||
test names need to be unique.
|
||||
:param marks: A single mark or a list of marks to be applied to this parameter set.
|
||||
:param id: The id to attribute to this parameter set.
|
||||
"""
|
||||
return ParameterSet.param(*values, marks=marks, id=id)
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
group._addoption( # private to use reserved lower-case short option
|
||||
group._addoption(
|
||||
"-k",
|
||||
action="store",
|
||||
dest="keyword",
|
||||
default="",
|
||||
metavar="EXPRESSION",
|
||||
help="Only run tests which match the given substring expression. "
|
||||
"An expression is a Python evaluable expression "
|
||||
"An expression is a Python evaluatable 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 "
|
||||
@@ -107,7 +89,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"The matching is case-insensitive.",
|
||||
)
|
||||
|
||||
group._addoption( # private to use reserved lower-case short option
|
||||
group._addoption(
|
||||
"-m",
|
||||
action="store",
|
||||
dest="markexpr",
|
||||
@@ -123,12 +105,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
help="show markers (builtin, plugin and per-project ones).",
|
||||
)
|
||||
|
||||
parser.addini("markers", "Register new markers for test functions", "linelist")
|
||||
parser.addini("markers", "Markers for test functions", "linelist")
|
||||
parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
import _pytest.config
|
||||
|
||||
if config.option.markers:
|
||||
@@ -138,7 +120,7 @@ def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
|
||||
parts = line.split(":", 1)
|
||||
name = parts[0]
|
||||
rest = parts[1] if len(parts) == 2 else ""
|
||||
tw.write(f"@pytest.mark.{name}:", bold=True)
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.line(rest)
|
||||
tw.line()
|
||||
config._ensure_unconfigure()
|
||||
@@ -167,22 +149,15 @@ class KeywordMatcher:
|
||||
_names: AbstractSet[str]
|
||||
|
||||
@classmethod
|
||||
def from_item(cls, item: Item) -> KeywordMatcher:
|
||||
def from_item(cls, item: "Item") -> "KeywordMatcher":
|
||||
mapped_names = set()
|
||||
|
||||
# Add the names of the current item and any parent items,
|
||||
# except the Session and root Directory's which are not
|
||||
# interesting for matching.
|
||||
# Add the names of the current item and any parent items.
|
||||
import pytest
|
||||
|
||||
for node in item.listchain():
|
||||
if isinstance(node, pytest.Session):
|
||||
continue
|
||||
if isinstance(node, pytest.Directory) and isinstance(
|
||||
node.parent, pytest.Session
|
||||
):
|
||||
continue
|
||||
mapped_names.add(node.name)
|
||||
if not isinstance(node, pytest.Session):
|
||||
mapped_names.add(node.name)
|
||||
|
||||
# Add the names added as extra keywords to current or parent items.
|
||||
mapped_names.update(item.listextrakeywords())
|
||||
@@ -197,14 +172,17 @@ class KeywordMatcher:
|
||||
|
||||
return cls(mapped_names)
|
||||
|
||||
def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool:
|
||||
if kwargs:
|
||||
raise UsageError("Keyword expressions do not support call parameters.")
|
||||
def __call__(self, subname: str) -> bool:
|
||||
subname = subname.lower()
|
||||
return any(subname in name.lower() for name in self._names)
|
||||
names = (name.lower() for name in self._names)
|
||||
|
||||
for name in names:
|
||||
if subname in name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def deselect_by_keyword(items: list[Item], config: Config) -> None:
|
||||
def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
|
||||
keywordexpr = config.option.keyword.lstrip()
|
||||
if not keywordexpr:
|
||||
return
|
||||
@@ -231,37 +209,29 @@ class MarkMatcher:
|
||||
Tries to match on any marker names, attached to the given colitem.
|
||||
"""
|
||||
|
||||
__slots__ = ("own_mark_name_mapping",)
|
||||
__slots__ = ("own_mark_names",)
|
||||
|
||||
own_mark_name_mapping: dict[str, list[Mark]]
|
||||
own_mark_names: AbstractSet[str]
|
||||
|
||||
@classmethod
|
||||
def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher:
|
||||
mark_name_mapping = collections.defaultdict(list)
|
||||
for mark in markers:
|
||||
mark_name_mapping[mark.name].append(mark)
|
||||
return cls(mark_name_mapping)
|
||||
def from_item(cls, item: "Item") -> "MarkMatcher":
|
||||
mark_names = {mark.name for mark in item.iter_markers()}
|
||||
return cls(mark_names)
|
||||
|
||||
def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool:
|
||||
if not (matches := self.own_mark_name_mapping.get(name, [])):
|
||||
return False
|
||||
|
||||
for mark in matches: # pylint: disable=consider-using-any-or-all
|
||||
if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()):
|
||||
return True
|
||||
return False
|
||||
def __call__(self, name: str) -> bool:
|
||||
return name in self.own_mark_names
|
||||
|
||||
|
||||
def deselect_by_mark(items: list[Item], config: Config) -> None:
|
||||
def deselect_by_mark(items: "List[Item]", config: Config) -> None:
|
||||
matchexpr = config.option.markexpr
|
||||
if not matchexpr:
|
||||
return
|
||||
|
||||
expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
|
||||
remaining: list[Item] = []
|
||||
deselected: list[Item] = []
|
||||
remaining: List[Item] = []
|
||||
deselected: List[Item] = []
|
||||
for item in items:
|
||||
if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())):
|
||||
if expr.evaluate(MarkMatcher.from_item(item)):
|
||||
remaining.append(item)
|
||||
else:
|
||||
deselected.append(item)
|
||||
@@ -273,13 +243,11 @@ def deselect_by_mark(items: list[Item], config: Config) -> None:
|
||||
def _parse_expression(expr: str, exc_message: str) -> Expression:
|
||||
try:
|
||||
return Expression.compile(expr)
|
||||
except SyntaxError as e:
|
||||
raise UsageError(
|
||||
f"{exc_message}: {e.text}: at column {e.offset}: {e.msg}"
|
||||
) from None
|
||||
except ParseError as e:
|
||||
raise UsageError(f"{exc_message}: {expr}: {e}") from None
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items: list[Item], config: Config) -> None:
|
||||
def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None:
|
||||
deselect_by_keyword(items, config)
|
||||
deselect_by_mark(items, config)
|
||||
|
||||
@@ -292,8 +260,8 @@ def pytest_configure(config: Config) -> None:
|
||||
|
||||
if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
|
||||
raise UsageError(
|
||||
f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect"
|
||||
f" but it is {empty_parameterset!r}"
|
||||
"{!s} must be one of skip, xfail or fail_at_collect"
|
||||
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,49 +5,40 @@ The grammar is:
|
||||
expression: expr? EOF
|
||||
expr: and_expr ('or' and_expr)*
|
||||
and_expr: not_expr ('and' not_expr)*
|
||||
not_expr: 'not' not_expr | '(' expr ')' | ident kwargs?
|
||||
|
||||
not_expr: 'not' not_expr | '(' expr ')' | ident
|
||||
ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
|
||||
kwargs: ('(' name '=' value ( ', ' name '=' value )* ')')
|
||||
name: a valid ident, but not a reserved keyword
|
||||
value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None'
|
||||
|
||||
The semantics are:
|
||||
|
||||
- Empty expression evaluates to False.
|
||||
- ident evaluates to True or False according to a provided matcher function.
|
||||
- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
|
||||
- ident evaluates to True of False according to a provided matcher function.
|
||||
- or/and/not evaluate according to the usual boolean semantics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
import enum
|
||||
import keyword
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import Literal
|
||||
from typing import Callable
|
||||
from typing import Iterator
|
||||
from typing import Mapping
|
||||
from typing import NoReturn
|
||||
from typing import overload
|
||||
from typing import Protocol
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
astNameConstant = ast.Constant
|
||||
else:
|
||||
astNameConstant = ast.NameConstant
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Expression",
|
||||
"ExpressionMatcher",
|
||||
"ParseError",
|
||||
]
|
||||
|
||||
|
||||
FILE_NAME: Final = "<pytest match expression>"
|
||||
|
||||
|
||||
class TokenType(enum.Enum):
|
||||
LPAREN = "left parenthesis"
|
||||
RPAREN = "right parenthesis"
|
||||
@@ -56,24 +47,35 @@ class TokenType(enum.Enum):
|
||||
NOT = "not"
|
||||
IDENT = "identifier"
|
||||
EOF = "end of input"
|
||||
EQUAL = "="
|
||||
STRING = "string literal"
|
||||
COMMA = ","
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Token:
|
||||
__slots__ = ("pos", "type", "value")
|
||||
__slots__ = ("type", "value", "pos")
|
||||
type: TokenType
|
||||
value: str
|
||||
pos: int
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
"""The expression contains invalid syntax.
|
||||
|
||||
:param column: The column in the line where the error occurred (1-based).
|
||||
:param message: A description of the error.
|
||||
"""
|
||||
|
||||
def __init__(self, column: int, message: str) -> None:
|
||||
self.column = column
|
||||
self.message = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"at column {self.column}: {self.message}"
|
||||
|
||||
|
||||
class Scanner:
|
||||
__slots__ = ("current", "input", "tokens")
|
||||
__slots__ = ("tokens", "current")
|
||||
|
||||
def __init__(self, input: str) -> None:
|
||||
self.input = input
|
||||
self.tokens = self.lex(input)
|
||||
self.current = next(self.tokens)
|
||||
|
||||
@@ -88,27 +90,6 @@ class Scanner:
|
||||
elif input[pos] == ")":
|
||||
yield Token(TokenType.RPAREN, ")", pos)
|
||||
pos += 1
|
||||
elif input[pos] == "=":
|
||||
yield Token(TokenType.EQUAL, "=", pos)
|
||||
pos += 1
|
||||
elif input[pos] == ",":
|
||||
yield Token(TokenType.COMMA, ",", pos)
|
||||
pos += 1
|
||||
elif (quote_char := input[pos]) in ("'", '"'):
|
||||
end_quote_pos = input.find(quote_char, pos + 1)
|
||||
if end_quote_pos == -1:
|
||||
raise SyntaxError(
|
||||
f'closing quote "{quote_char}" is missing',
|
||||
(FILE_NAME, 1, pos + 1, input),
|
||||
)
|
||||
value = input[pos : end_quote_pos + 1]
|
||||
if (backslash_pos := input.find("\\")) != -1:
|
||||
raise SyntaxError(
|
||||
r'escaping with "\" not supported in marker expression',
|
||||
(FILE_NAME, 1, backslash_pos + 1, input),
|
||||
)
|
||||
yield Token(TokenType.STRING, value, pos)
|
||||
pos += len(value)
|
||||
else:
|
||||
match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
|
||||
if match:
|
||||
@@ -123,21 +104,13 @@ class Scanner:
|
||||
yield Token(TokenType.IDENT, value, pos)
|
||||
pos += len(value)
|
||||
else:
|
||||
raise SyntaxError(
|
||||
raise ParseError(
|
||||
pos + 1,
|
||||
f'unexpected character "{input[pos]}"',
|
||||
(FILE_NAME, 1, pos + 1, input),
|
||||
)
|
||||
yield Token(TokenType.EOF, "", pos)
|
||||
|
||||
@overload
|
||||
def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...
|
||||
|
||||
@overload
|
||||
def accept(
|
||||
self, type: TokenType, *, reject: Literal[False] = False
|
||||
) -> Token | None: ...
|
||||
|
||||
def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
|
||||
def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]:
|
||||
if self.current.type is type:
|
||||
token = self.current
|
||||
if token.type is not TokenType.EOF:
|
||||
@@ -148,12 +121,12 @@ class Scanner:
|
||||
return None
|
||||
|
||||
def reject(self, expected: Sequence[TokenType]) -> NoReturn:
|
||||
raise SyntaxError(
|
||||
raise ParseError(
|
||||
self.current.pos + 1,
|
||||
"expected {}; got {}".format(
|
||||
" OR ".join(type.value for type in expected),
|
||||
self.current.type.value,
|
||||
),
|
||||
(FILE_NAME, 1, self.current.pos + 1, self.input),
|
||||
)
|
||||
|
||||
|
||||
@@ -165,7 +138,7 @@ IDENT_PREFIX = "$"
|
||||
|
||||
def expression(s: Scanner) -> ast.Expression:
|
||||
if s.accept(TokenType.EOF):
|
||||
ret: ast.expr = ast.Constant(False)
|
||||
ret: ast.expr = astNameConstant(False)
|
||||
else:
|
||||
ret = expr(s)
|
||||
s.accept(TokenType.EOF, reject=True)
|
||||
@@ -197,108 +170,18 @@ def not_expr(s: Scanner) -> ast.expr:
|
||||
return ret
|
||||
ident = s.accept(TokenType.IDENT)
|
||||
if ident:
|
||||
name = ast.Name(IDENT_PREFIX + ident.value, ast.Load())
|
||||
if s.accept(TokenType.LPAREN):
|
||||
ret = ast.Call(func=name, args=[], keywords=all_kwargs(s))
|
||||
s.accept(TokenType.RPAREN, reject=True)
|
||||
else:
|
||||
ret = name
|
||||
return ret
|
||||
|
||||
return ast.Name(IDENT_PREFIX + ident.value, ast.Load())
|
||||
s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
|
||||
|
||||
|
||||
BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}
|
||||
|
||||
|
||||
def single_kwarg(s: Scanner) -> ast.keyword:
|
||||
keyword_name = s.accept(TokenType.IDENT, reject=True)
|
||||
if not keyword_name.value.isidentifier():
|
||||
raise SyntaxError(
|
||||
f"not a valid python identifier {keyword_name.value}",
|
||||
(FILE_NAME, 1, keyword_name.pos + 1, s.input),
|
||||
)
|
||||
if keyword.iskeyword(keyword_name.value):
|
||||
raise SyntaxError(
|
||||
f"unexpected reserved python keyword `{keyword_name.value}`",
|
||||
(FILE_NAME, 1, keyword_name.pos + 1, s.input),
|
||||
)
|
||||
s.accept(TokenType.EQUAL, reject=True)
|
||||
|
||||
if value_token := s.accept(TokenType.STRING):
|
||||
value: str | int | bool | None = value_token.value[1:-1] # strip quotes
|
||||
else:
|
||||
value_token = s.accept(TokenType.IDENT, reject=True)
|
||||
if (number := value_token.value).isdigit() or (
|
||||
number.startswith("-") and number[1:].isdigit()
|
||||
):
|
||||
value = int(number)
|
||||
elif value_token.value in BUILTIN_MATCHERS:
|
||||
value = BUILTIN_MATCHERS[value_token.value]
|
||||
else:
|
||||
raise SyntaxError(
|
||||
f'unexpected character/s "{value_token.value}"',
|
||||
(FILE_NAME, 1, value_token.pos + 1, s.input),
|
||||
)
|
||||
|
||||
ret = ast.keyword(keyword_name.value, ast.Constant(value))
|
||||
return ret
|
||||
|
||||
|
||||
def all_kwargs(s: Scanner) -> list[ast.keyword]:
|
||||
ret = [single_kwarg(s)]
|
||||
while s.accept(TokenType.COMMA):
|
||||
ret.append(single_kwarg(s))
|
||||
return ret
|
||||
|
||||
|
||||
class ExpressionMatcher(Protocol):
|
||||
"""A callable which, given an identifier and optional kwargs, should return
|
||||
whether it matches in an :class:`Expression` evaluation.
|
||||
|
||||
Should be prepared to handle arbitrary strings as input.
|
||||
|
||||
If no kwargs are provided, the expression of the form `foo`.
|
||||
If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`.
|
||||
|
||||
If the expression is not supported (e.g. don't want to accept the kwargs
|
||||
syntax variant), should raise :class:`~pytest.UsageError`.
|
||||
|
||||
Example::
|
||||
|
||||
def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool:
|
||||
# Match `cat`.
|
||||
if name == "cat" and not kwargs:
|
||||
return True
|
||||
# Match `dog(barks=True)`.
|
||||
if name == "dog" and kwargs == {"barks": False}:
|
||||
return True
|
||||
return False
|
||||
"""
|
||||
|
||||
def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ...
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class MatcherNameAdapter:
|
||||
matcher: ExpressionMatcher
|
||||
name: str
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.matcher(self.name)
|
||||
|
||||
def __call__(self, **kwargs: str | int | bool | None) -> bool:
|
||||
return self.matcher(self.name, **kwargs)
|
||||
|
||||
|
||||
class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
|
||||
class MatcherAdapter(Mapping[str, bool]):
|
||||
"""Adapts a matcher function to a locals mapping as required by eval()."""
|
||||
|
||||
def __init__(self, matcher: ExpressionMatcher) -> None:
|
||||
def __init__(self, matcher: Callable[[str], bool]) -> None:
|
||||
self.matcher = matcher
|
||||
|
||||
def __getitem__(self, key: str) -> MatcherNameAdapter:
|
||||
return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :])
|
||||
def __getitem__(self, key: str) -> bool:
|
||||
return self.matcher(key[len(IDENT_PREFIX) :])
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
raise NotImplementedError()
|
||||
@@ -307,47 +190,39 @@ class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@final
|
||||
class Expression:
|
||||
"""A compiled match expression as used by -k and -m.
|
||||
|
||||
The expression can be evaluated against different matchers.
|
||||
"""
|
||||
|
||||
__slots__ = ("_code", "input")
|
||||
__slots__ = ("code",)
|
||||
|
||||
def __init__(self, input: str, code: types.CodeType) -> None:
|
||||
#: The original input line, as a string.
|
||||
self.input: Final = input
|
||||
self._code: Final = code
|
||||
def __init__(self, code: types.CodeType) -> None:
|
||||
self.code = code
|
||||
|
||||
@classmethod
|
||||
def compile(cls, input: str) -> Expression:
|
||||
def compile(self, input: str) -> "Expression":
|
||||
"""Compile a match expression.
|
||||
|
||||
:param input: The input expression - one line.
|
||||
|
||||
:raises SyntaxError: If the expression is malformed.
|
||||
"""
|
||||
astexpr = expression(Scanner(input))
|
||||
code = compile(
|
||||
code: types.CodeType = compile(
|
||||
astexpr,
|
||||
filename="<pytest match expression>",
|
||||
mode="eval",
|
||||
)
|
||||
return Expression(input, code)
|
||||
return Expression(code)
|
||||
|
||||
def evaluate(self, matcher: ExpressionMatcher) -> bool:
|
||||
def evaluate(self, matcher: Callable[[str], bool]) -> bool:
|
||||
"""Evaluate the match expression.
|
||||
|
||||
:param matcher:
|
||||
A callback which determines whether an identifier matches or not.
|
||||
See the :class:`ExpressionMatcher` protocol for details and example.
|
||||
Given an identifier, should return whether it matches or not.
|
||||
Should be prepared to handle arbitrary strings as input.
|
||||
|
||||
:returns: Whether the expression matches or not.
|
||||
|
||||
:raises UsageError:
|
||||
If the matcher doesn't support the expression. Cannot happen if the
|
||||
matcher supports all expressions.
|
||||
"""
|
||||
return bool(eval(self._code, {"__builtins__": {}}, MatcherAdapter(matcher)))
|
||||
ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))
|
||||
return ret
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import MutableMapping
|
||||
from collections.abc import Sequence
|
||||
import dataclasses
|
||||
import enum
|
||||
import inspect
|
||||
import warnings
|
||||
from typing import Any
|
||||
from typing import final
|
||||
from typing import Callable
|
||||
from typing import Collection
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import MutableMapping
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
import warnings
|
||||
from typing import Union
|
||||
|
||||
from .._code import getfslineno
|
||||
from ..compat import ascii_escaped
|
||||
from ..compat import final
|
||||
from ..compat import NOTSET
|
||||
from ..compat import NotSetType
|
||||
from _pytest.config import Config
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import MARKED_FIXTURE
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.raises import AbstractRaises
|
||||
from _pytest.scope import _ScopeName
|
||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..nodes import Node
|
||||
|
||||
@@ -39,37 +38,33 @@ if TYPE_CHECKING:
|
||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||
|
||||
|
||||
# Singleton type for HIDDEN_PARAM, as described in:
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class _HiddenParam(enum.Enum):
|
||||
token = 0
|
||||
|
||||
|
||||
#: Can be used as a parameter set id to hide it from the test name.
|
||||
HIDDEN_PARAM = _HiddenParam.token
|
||||
|
||||
|
||||
def istestfunc(func) -> bool:
|
||||
return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||
|
||||
|
||||
def get_empty_parameterset_mark(
|
||||
config: Config, argnames: Sequence[str], func
|
||||
) -> MarkDecorator:
|
||||
) -> "MarkDecorator":
|
||||
from ..nodes import Collector
|
||||
|
||||
argslisting = ", ".join(argnames)
|
||||
fs, lineno = getfslineno(func)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames,
|
||||
func.__name__,
|
||||
fs,
|
||||
lineno,
|
||||
)
|
||||
|
||||
_fs, lineno = getfslineno(func)
|
||||
reason = f"got empty parameter set for ({argslisting})"
|
||||
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
if requested_mark in ("", None, "skip"):
|
||||
mark = MARK_GEN.skip(reason=reason)
|
||||
elif requested_mark == "xfail":
|
||||
mark = MARK_GEN.xfail(reason=reason, run=False)
|
||||
elif requested_mark == "fail_at_collect":
|
||||
f_name = func.__name__
|
||||
_, lineno = getfslineno(func)
|
||||
raise Collector.CollectError(
|
||||
f"Empty parameter set in '{func.__name__}' at line {lineno + 1}"
|
||||
"Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
|
||||
)
|
||||
else:
|
||||
raise LookupError(requested_mark)
|
||||
@@ -77,68 +72,34 @@ def get_empty_parameterset_mark(
|
||||
|
||||
|
||||
class ParameterSet(NamedTuple):
|
||||
"""A set of values for a set of parameters along with associated marks and
|
||||
an optional ID for the set.
|
||||
|
||||
Examples::
|
||||
|
||||
pytest.param(1, 2, 3)
|
||||
# ParameterSet(values=(1, 2, 3), marks=(), id=None)
|
||||
|
||||
pytest.param("hello", id="greeting")
|
||||
# ParameterSet(values=("hello",), marks=(), id="greeting")
|
||||
|
||||
# Parameter set with marks
|
||||
pytest.param(42, marks=pytest.mark.xfail)
|
||||
# ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None)
|
||||
|
||||
# From parametrize mark (parameter names + list of parameter sets)
|
||||
pytest.mark.parametrize(
|
||||
("a", "b", "expected"),
|
||||
[
|
||||
(1, 2, 3),
|
||||
pytest.param(40, 2, 42, id="everything"),
|
||||
],
|
||||
)
|
||||
# ParameterSet(values=(1, 2, 3), marks=(), id=None)
|
||||
# ParameterSet(values=(40, 2, 42), marks=(), id="everything")
|
||||
"""
|
||||
|
||||
values: Sequence[object | NotSetType]
|
||||
marks: Collection[MarkDecorator | Mark]
|
||||
id: str | _HiddenParam | None
|
||||
values: Sequence[Union[object, NotSetType]]
|
||||
marks: Collection[Union["MarkDecorator", "Mark"]]
|
||||
id: Optional[str]
|
||||
|
||||
@classmethod
|
||||
def param(
|
||||
cls,
|
||||
*values: object,
|
||||
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
|
||||
id: str | _HiddenParam | None = None,
|
||||
) -> ParameterSet:
|
||||
marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
|
||||
id: Optional[str] = None,
|
||||
) -> "ParameterSet":
|
||||
if isinstance(marks, MarkDecorator):
|
||||
marks = (marks,)
|
||||
else:
|
||||
assert isinstance(marks, collections.abc.Collection)
|
||||
if any(i.name == "usefixtures" for i in marks):
|
||||
raise ValueError(
|
||||
"pytest.param cannot add pytest.mark.usefixtures; see "
|
||||
"https://docs.pytest.org/en/stable/reference/reference.html#pytest-param"
|
||||
)
|
||||
|
||||
if id is not None:
|
||||
if not isinstance(id, str) and id is not HIDDEN_PARAM:
|
||||
raise TypeError(
|
||||
"Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, "
|
||||
f"got {type(id)}: {id!r}",
|
||||
)
|
||||
if not isinstance(id, str):
|
||||
raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
|
||||
id = ascii_escaped(id)
|
||||
return cls(values, marks, id)
|
||||
|
||||
@classmethod
|
||||
def extract_from(
|
||||
cls,
|
||||
parameterset: ParameterSet | Sequence[object] | object,
|
||||
parameterset: Union["ParameterSet", Sequence[object], object],
|
||||
force_tuple: bool = False,
|
||||
) -> ParameterSet:
|
||||
) -> "ParameterSet":
|
||||
"""Extract from an object or objects.
|
||||
|
||||
:param parameterset:
|
||||
@@ -149,6 +110,7 @@ class ParameterSet(NamedTuple):
|
||||
Enforce tuple wrapping so single argument tuple values
|
||||
don't get decomposed and break tests.
|
||||
"""
|
||||
|
||||
if isinstance(parameterset, cls):
|
||||
return parameterset
|
||||
if force_tuple:
|
||||
@@ -163,11 +125,11 @@ class ParameterSet(NamedTuple):
|
||||
|
||||
@staticmethod
|
||||
def _parse_parametrize_args(
|
||||
argnames: str | Sequence[str],
|
||||
argvalues: Iterable[ParameterSet | Sequence[object] | object],
|
||||
argnames: Union[str, Sequence[str]],
|
||||
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> tuple[Sequence[str], bool]:
|
||||
) -> Tuple[Sequence[str], bool]:
|
||||
if isinstance(argnames, str):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
force_tuple = len(argnames) == 1
|
||||
@@ -177,9 +139,9 @@ class ParameterSet(NamedTuple):
|
||||
|
||||
@staticmethod
|
||||
def _parse_parametrize_parameters(
|
||||
argvalues: Iterable[ParameterSet | Sequence[object] | object],
|
||||
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
|
||||
force_tuple: bool,
|
||||
) -> list[ParameterSet]:
|
||||
) -> List["ParameterSet"]:
|
||||
return [
|
||||
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
|
||||
]
|
||||
@@ -187,12 +149,12 @@ class ParameterSet(NamedTuple):
|
||||
@classmethod
|
||||
def _for_parametrize(
|
||||
cls,
|
||||
argnames: str | Sequence[str],
|
||||
argvalues: Iterable[ParameterSet | Sequence[object] | object],
|
||||
argnames: Union[str, Sequence[str]],
|
||||
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
|
||||
func,
|
||||
config: Config,
|
||||
nodeid: str,
|
||||
) -> tuple[Sequence[str], list[ParameterSet]]:
|
||||
) -> Tuple[Sequence[str], List["ParameterSet"]]:
|
||||
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
|
||||
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
|
||||
del argvalues
|
||||
@@ -222,9 +184,7 @@ class ParameterSet(NamedTuple):
|
||||
# parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
|
||||
mark = get_empty_parameterset_mark(config, argnames, func)
|
||||
parameters.append(
|
||||
ParameterSet(
|
||||
values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET"
|
||||
)
|
||||
ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
|
||||
)
|
||||
return argnames, parameters
|
||||
|
||||
@@ -237,24 +197,24 @@ class Mark:
|
||||
#: Name of the mark.
|
||||
name: str
|
||||
#: Positional arguments of the mark decorator.
|
||||
args: tuple[Any, ...]
|
||||
args: Tuple[Any, ...]
|
||||
#: Keyword arguments of the mark decorator.
|
||||
kwargs: Mapping[str, Any]
|
||||
|
||||
#: Source Mark for ids with parametrize Marks.
|
||||
_param_ids_from: Mark | None = dataclasses.field(default=None, repr=False)
|
||||
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
|
||||
#: Resolved/generated ids with parametrize Marks.
|
||||
_param_ids_generated: Sequence[str] | None = dataclasses.field(
|
||||
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
|
||||
default=None, repr=False
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
args: tuple[Any, ...],
|
||||
args: Tuple[Any, ...],
|
||||
kwargs: Mapping[str, Any],
|
||||
param_ids_from: Mark | None = None,
|
||||
param_ids_generated: Sequence[str] | None = None,
|
||||
param_ids_from: Optional["Mark"] = None,
|
||||
param_ids_generated: Optional[Sequence[str]] = None,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
@@ -270,7 +230,7 @@ class Mark:
|
||||
def _has_param_ids(self) -> bool:
|
||||
return "ids" in self.kwargs or len(self.args) >= 4
|
||||
|
||||
def combined_with(self, other: Mark) -> Mark:
|
||||
def combined_with(self, other: "Mark") -> "Mark":
|
||||
"""Return a new Mark which is a combination of this
|
||||
Mark and another Mark.
|
||||
|
||||
@@ -282,7 +242,7 @@ class Mark:
|
||||
assert self.name == other.name
|
||||
|
||||
# Remember source of ids with parametrize Marks.
|
||||
param_ids_from: Mark | None = None
|
||||
param_ids_from: Optional[Mark] = None
|
||||
if self.name == "parametrize":
|
||||
if other._has_param_ids():
|
||||
param_ids_from = other
|
||||
@@ -301,7 +261,7 @@ class Mark:
|
||||
# A generic parameter designating an object to which a Mark may
|
||||
# be applied -- a test function (callable) or class.
|
||||
# Note: a lambda is not allowed, but this can't be represented.
|
||||
Markable = TypeVar("Markable", bound=Callable[..., object] | type)
|
||||
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -310,8 +270,8 @@ class MarkDecorator:
|
||||
|
||||
``MarkDecorators`` are created with ``pytest.mark``::
|
||||
|
||||
mark1 = pytest.mark.NAME # Simple MarkDecorator
|
||||
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
|
||||
mark1 = pytest.mark.NAME # Simple MarkDecorator
|
||||
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
|
||||
|
||||
and can then be applied as decorators to test functions::
|
||||
|
||||
@@ -353,7 +313,7 @@ class MarkDecorator:
|
||||
return self.mark.name
|
||||
|
||||
@property
|
||||
def args(self) -> tuple[Any, ...]:
|
||||
def args(self) -> Tuple[Any, ...]:
|
||||
"""Alias for mark.args."""
|
||||
return self.mark.args
|
||||
|
||||
@@ -367,7 +327,7 @@ class MarkDecorator:
|
||||
""":meta private:"""
|
||||
return self.name # for backward-compat (2.4.1 had this attr)
|
||||
|
||||
def with_args(self, *args: object, **kwargs: object) -> MarkDecorator:
|
||||
def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
|
||||
"""Return a MarkDecorator with extra arguments added.
|
||||
|
||||
Unlike calling the MarkDecorator, with_args() can be used even
|
||||
@@ -380,11 +340,11 @@ class MarkDecorator:
|
||||
# return type. Not much we can do about that. Thankfully mypy picks
|
||||
# the first match so it works out even if we break the rules.
|
||||
@overload
|
||||
def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
|
||||
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __call__(self, *args: object, **kwargs: object) -> MarkDecorator:
|
||||
def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
|
||||
pass
|
||||
|
||||
def __call__(self, *args: object, **kwargs: object):
|
||||
@@ -392,22 +352,17 @@ class MarkDecorator:
|
||||
if args and not kwargs:
|
||||
func = args[0]
|
||||
is_class = inspect.isclass(func)
|
||||
# For staticmethods/classmethods, the marks are eventually fetched from the
|
||||
# function object, not the descriptor, so unwrap.
|
||||
unwrapped_func = func
|
||||
if isinstance(func, staticmethod | classmethod):
|
||||
unwrapped_func = func.__func__
|
||||
if len(args) == 1 and (istestfunc(unwrapped_func) or is_class):
|
||||
store_mark(unwrapped_func, self.mark, stacklevel=3)
|
||||
if len(args) == 1 and (istestfunc(func) or is_class):
|
||||
store_mark(func, self.mark)
|
||||
return func
|
||||
return self.with_args(*args, **kwargs)
|
||||
|
||||
|
||||
def get_unpacked_marks(
|
||||
obj: object | type,
|
||||
obj: Union[object, type],
|
||||
*,
|
||||
consider_mro: bool = True,
|
||||
) -> list[Mark]:
|
||||
) -> List[Mark]:
|
||||
"""Obtain the unpacked marks that are stored on an object.
|
||||
|
||||
If obj is a class and consider_mro is true, return marks applied to
|
||||
@@ -437,7 +392,7 @@ def get_unpacked_marks(
|
||||
|
||||
|
||||
def normalize_mark_list(
|
||||
mark_list: Iterable[Mark | MarkDecorator],
|
||||
mark_list: Iterable[Union[Mark, MarkDecorator]]
|
||||
) -> Iterable[Mark]:
|
||||
"""
|
||||
Normalize an iterable of Mark or MarkDecorator objects into a list of marks
|
||||
@@ -449,22 +404,16 @@ def normalize_mark_list(
|
||||
for mark in mark_list:
|
||||
mark_obj = getattr(mark, "mark", mark)
|
||||
if not isinstance(mark_obj, Mark):
|
||||
raise TypeError(f"got {mark_obj!r} instead of Mark")
|
||||
raise TypeError(f"got {repr(mark_obj)} instead of Mark")
|
||||
yield mark_obj
|
||||
|
||||
|
||||
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
|
||||
def store_mark(obj, mark: Mark) -> None:
|
||||
"""Store a Mark on an object.
|
||||
|
||||
This is used to implement the Mark declarations/decorators correctly.
|
||||
"""
|
||||
assert isinstance(mark, Mark), mark
|
||||
|
||||
from ..fixtures import getfixturemarker
|
||||
|
||||
if getfixturemarker(obj) is not None:
|
||||
warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
|
||||
|
||||
# Always reassign name to avoid updating pytestmark in a reference that
|
||||
# was only borrowed.
|
||||
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
|
||||
@@ -473,52 +422,59 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
|
||||
# Typing for builtin pytest marks. This is cheating; it gives builtin marks
|
||||
# special privilege, and breaks modularity. But practicality beats purity...
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.scope import _ScopeName
|
||||
|
||||
class _SkipMarkDecorator(MarkDecorator):
|
||||
@overload # type: ignore[override,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable: ...
|
||||
@overload # type: ignore[override,misc,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __call__(self, reason: str = ...) -> MarkDecorator: ...
|
||||
def __call__(self, reason: str = ...) -> "MarkDecorator":
|
||||
...
|
||||
|
||||
class _SkipifMarkDecorator(MarkDecorator):
|
||||
def __call__( # type: ignore[override]
|
||||
self,
|
||||
condition: str | bool = ...,
|
||||
*conditions: str | bool,
|
||||
condition: Union[str, bool] = ...,
|
||||
*conditions: Union[str, bool],
|
||||
reason: str = ...,
|
||||
) -> MarkDecorator: ...
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
||||
class _XfailMarkDecorator(MarkDecorator):
|
||||
@overload # type: ignore[override,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable: ...
|
||||
@overload # type: ignore[override,misc,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __call__(
|
||||
self,
|
||||
condition: str | bool = False,
|
||||
*conditions: str | bool,
|
||||
condition: Union[str, bool] = ...,
|
||||
*conditions: Union[str, bool],
|
||||
reason: str = ...,
|
||||
run: bool = ...,
|
||||
raises: None
|
||||
| type[BaseException]
|
||||
| tuple[type[BaseException], ...]
|
||||
| AbstractRaises[BaseException] = ...,
|
||||
raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
|
||||
strict: bool = ...,
|
||||
) -> MarkDecorator: ...
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
||||
class _ParametrizeMarkDecorator(MarkDecorator):
|
||||
def __call__( # type: ignore[override]
|
||||
self,
|
||||
argnames: str | Sequence[str],
|
||||
argvalues: Iterable[ParameterSet | Sequence[object] | object],
|
||||
argnames: Union[str, Sequence[str]],
|
||||
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
|
||||
*,
|
||||
indirect: bool | Sequence[str] = ...,
|
||||
ids: Iterable[None | str | float | int | bool]
|
||||
| Callable[[Any], object | None]
|
||||
| None = ...,
|
||||
scope: _ScopeName | None = ...,
|
||||
) -> MarkDecorator: ...
|
||||
indirect: Union[bool, Sequence[str]] = ...,
|
||||
ids: Optional[
|
||||
Union[
|
||||
Iterable[Union[None, str, float, int, bool]],
|
||||
Callable[[Any], Optional[object]],
|
||||
]
|
||||
] = ...,
|
||||
scope: Optional[_ScopeName] = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
|
||||
class _UsefixturesMarkDecorator(MarkDecorator):
|
||||
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
|
||||
@@ -538,10 +494,9 @@ class MarkGenerator:
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.slowtest
|
||||
def test_function():
|
||||
pass
|
||||
pass
|
||||
|
||||
applies a 'slowtest' :class:`Mark` on ``test_function``.
|
||||
"""
|
||||
@@ -557,8 +512,8 @@ class MarkGenerator:
|
||||
|
||||
def __init__(self, *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._config: Config | None = None
|
||||
self._markers: set[str] = set()
|
||||
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."""
|
||||
@@ -580,24 +535,21 @@ class MarkGenerator:
|
||||
# If the name is not in the set of known marks after updating,
|
||||
# then it really is time to issue a warning or an error.
|
||||
if name not in self._markers:
|
||||
# Raise a specific error for common misspellings of "parametrize".
|
||||
if name in ["parameterize", "parametrise", "parameterise"]:
|
||||
__tracebackhide__ = True
|
||||
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
|
||||
|
||||
strict_markers = self._config.getini("strict_markers")
|
||||
if strict_markers is None:
|
||||
strict_markers = self._config.getini("strict")
|
||||
if strict_markers:
|
||||
if self._config.option.strict_markers or self._config.option.strict:
|
||||
fail(
|
||||
f"{name!r} not found in `markers` configuration option",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
# Raise a specific error for common misspellings of "parametrize".
|
||||
if name in ["parameterize", "parametrise", "parameterise"]:
|
||||
__tracebackhide__ = True
|
||||
fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
|
||||
|
||||
warnings.warn(
|
||||
f"Unknown pytest.mark.{name} - is this a typo? You can register "
|
||||
"Unknown pytest.mark.%s - is this a typo? You can register "
|
||||
"custom marks to avoid this warning - for details, see "
|
||||
"https://docs.pytest.org/en/stable/how-to/mark.html",
|
||||
"https://docs.pytest.org/en/stable/how-to/mark.html" % name,
|
||||
PytestUnknownMarkWarning,
|
||||
2,
|
||||
)
|
||||
@@ -610,9 +562,9 @@ MARK_GEN = MarkGenerator(_ispytest=True)
|
||||
|
||||
@final
|
||||
class NodeKeywords(MutableMapping[str, Any]):
|
||||
__slots__ = ("_markers", "node", "parent")
|
||||
__slots__ = ("node", "parent", "_markers")
|
||||
|
||||
def __init__(self, node: Node) -> None:
|
||||
def __init__(self, node: "Node") -> None:
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
@@ -632,13 +584,15 @@ class NodeKeywords(MutableMapping[str, Any]):
|
||||
# below and use the collections.abc fallback, but that would be slow.
|
||||
|
||||
def __contains__(self, key: object) -> bool:
|
||||
return key in self._markers or (
|
||||
self.parent is not None and key in self.parent.keywords
|
||||
return (
|
||||
key in self._markers
|
||||
or self.parent is not None
|
||||
and key in self.parent.keywords
|
||||
)
|
||||
|
||||
def update( # type: ignore[override]
|
||||
self,
|
||||
other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (),
|
||||
other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
|
||||
**kwds: Any,
|
||||
) -> None:
|
||||
self._markers.update(other)
|
||||
|
||||
Reference in New Issue
Block a user