Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/mark/structures.py: 47%
272 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1import collections.abc
2import inspect
3import warnings
4from typing import Any
5from typing import Callable
6from typing import Collection
7from typing import Iterable
8from typing import Iterator
9from typing import List
10from typing import Mapping
11from typing import MutableMapping
12from typing import NamedTuple
13from typing import Optional
14from typing import overload
15from typing import Sequence
16from typing import Set
17from typing import Tuple
18from typing import Type
19from typing import TYPE_CHECKING
20from typing import TypeVar
21from typing import Union
23import attr
25from .._code import getfslineno
26from ..compat import ascii_escaped
27from ..compat import final
28from ..compat import NOTSET
29from ..compat import NotSetType
30from _pytest.config import Config
31from _pytest.deprecated import check_ispytest
32from _pytest.outcomes import fail
33from _pytest.warning_types import PytestUnknownMarkWarning
35if TYPE_CHECKING:
36 from ..nodes import Node
39EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
42def istestfunc(func) -> bool:
43 return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"
46def get_empty_parameterset_mark(
47 config: Config, argnames: Sequence[str], func
48) -> "MarkDecorator":
49 from ..nodes import Collector
51 fs, lineno = getfslineno(func)
52 reason = "got empty parameter set %r, function %s at %s:%d" % (
53 argnames,
54 func.__name__,
55 fs,
56 lineno,
57 )
59 requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
60 if requested_mark in ("", None, "skip"):
61 mark = MARK_GEN.skip(reason=reason)
62 elif requested_mark == "xfail":
63 mark = MARK_GEN.xfail(reason=reason, run=False)
64 elif requested_mark == "fail_at_collect":
65 f_name = func.__name__
66 _, lineno = getfslineno(func)
67 raise Collector.CollectError(
68 "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
69 )
70 else:
71 raise LookupError(requested_mark)
72 return mark
75class ParameterSet(NamedTuple):
76 values: Sequence[Union[object, NotSetType]]
77 marks: Collection[Union["MarkDecorator", "Mark"]]
78 id: Optional[str]
80 @classmethod
81 def param(
82 cls,
83 *values: object,
84 marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (),
85 id: Optional[str] = None,
86 ) -> "ParameterSet":
87 if isinstance(marks, MarkDecorator):
88 marks = (marks,)
89 else:
90 assert isinstance(marks, collections.abc.Collection)
92 if id is not None:
93 if not isinstance(id, str):
94 raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
95 id = ascii_escaped(id)
96 return cls(values, marks, id)
98 @classmethod
99 def extract_from(
100 cls,
101 parameterset: Union["ParameterSet", Sequence[object], object],
102 force_tuple: bool = False,
103 ) -> "ParameterSet":
104 """Extract from an object or objects.
106 :param parameterset:
107 A legacy style parameterset that may or may not be a tuple,
108 and may or may not be wrapped into a mess of mark objects.
110 :param force_tuple:
111 Enforce tuple wrapping so single argument tuple values
112 don't get decomposed and break tests.
113 """
115 if isinstance(parameterset, cls):
116 return parameterset
117 if force_tuple:
118 return cls.param(parameterset)
119 else:
120 # TODO: Refactor to fix this type-ignore. Currently the following
121 # passes type-checking but crashes:
122 #
123 # @pytest.mark.parametrize(('x', 'y'), [1, 2])
124 # def test_foo(x, y): pass
125 return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
127 @staticmethod
128 def _parse_parametrize_args(
129 argnames: Union[str, Sequence[str]],
130 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
131 *args,
132 **kwargs,
133 ) -> Tuple[Sequence[str], bool]:
134 if isinstance(argnames, str):
135 argnames = [x.strip() for x in argnames.split(",") if x.strip()]
136 force_tuple = len(argnames) == 1
137 else:
138 force_tuple = False
139 return argnames, force_tuple
141 @staticmethod
142 def _parse_parametrize_parameters(
143 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
144 force_tuple: bool,
145 ) -> List["ParameterSet"]:
146 return [
147 ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
148 ]
150 @classmethod
151 def _for_parametrize(
152 cls,
153 argnames: Union[str, Sequence[str]],
154 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
155 func,
156 config: Config,
157 nodeid: str,
158 ) -> Tuple[Sequence[str], List["ParameterSet"]]:
159 argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
160 parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
161 del argvalues
163 if parameters:
164 # Check all parameter sets have the correct number of values.
165 for param in parameters:
166 if len(param.values) != len(argnames):
167 msg = (
168 '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
169 " {names}\n"
170 "must be equal to the number of values ({values_len}):\n"
171 " {values}"
172 )
173 fail(
174 msg.format(
175 nodeid=nodeid,
176 values=param.values,
177 names=argnames,
178 names_len=len(argnames),
179 values_len=len(param.values),
180 ),
181 pytrace=False,
182 )
183 else:
184 # Empty parameter set (likely computed at runtime): create a single
185 # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
186 mark = get_empty_parameterset_mark(config, argnames, func)
187 parameters.append(
188 ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
189 )
190 return argnames, parameters
193@final
194@attr.s(frozen=True, init=False, auto_attribs=True)
195class Mark:
196 #: Name of the mark.
197 name: str
198 #: Positional arguments of the mark decorator.
199 args: Tuple[Any, ...]
200 #: Keyword arguments of the mark decorator.
201 kwargs: Mapping[str, Any]
203 #: Source Mark for ids with parametrize Marks.
204 _param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
205 #: Resolved/generated ids with parametrize Marks.
206 _param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
208 def __init__(
209 self,
210 name: str,
211 args: Tuple[Any, ...],
212 kwargs: Mapping[str, Any],
213 param_ids_from: Optional["Mark"] = None,
214 param_ids_generated: Optional[Sequence[str]] = None,
215 *,
216 _ispytest: bool = False,
217 ) -> None:
218 """:meta private:"""
219 check_ispytest(_ispytest)
220 # Weirdness to bypass frozen=True.
221 object.__setattr__(self, "name", name)
222 object.__setattr__(self, "args", args)
223 object.__setattr__(self, "kwargs", kwargs)
224 object.__setattr__(self, "_param_ids_from", param_ids_from)
225 object.__setattr__(self, "_param_ids_generated", param_ids_generated)
227 def _has_param_ids(self) -> bool:
228 return "ids" in self.kwargs or len(self.args) >= 4
230 def combined_with(self, other: "Mark") -> "Mark":
231 """Return a new Mark which is a combination of this
232 Mark and another Mark.
234 Combines by appending args and merging kwargs.
236 :param Mark other: The mark to combine with.
237 :rtype: Mark
238 """
239 assert self.name == other.name
241 # Remember source of ids with parametrize Marks.
242 param_ids_from: Optional[Mark] = None
243 if self.name == "parametrize":
244 if other._has_param_ids():
245 param_ids_from = other
246 elif self._has_param_ids():
247 param_ids_from = self
249 return Mark(
250 self.name,
251 self.args + other.args,
252 dict(self.kwargs, **other.kwargs),
253 param_ids_from=param_ids_from,
254 _ispytest=True,
255 )
258# A generic parameter designating an object to which a Mark may
259# be applied -- a test function (callable) or class.
260# Note: a lambda is not allowed, but this can't be represented.
261Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
264@attr.s(init=False, auto_attribs=True)
265class MarkDecorator:
266 """A decorator for applying a mark on test functions and classes.
268 ``MarkDecorators`` are created with ``pytest.mark``::
270 mark1 = pytest.mark.NAME # Simple MarkDecorator
271 mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
273 and can then be applied as decorators to test functions::
275 @mark2
276 def test_function():
277 pass
279 When a ``MarkDecorator`` is called, it does the following:
281 1. If called with a single class as its only positional argument and no
282 additional keyword arguments, it attaches the mark to the class so it
283 gets applied automatically to all test cases found in that class.
285 2. If called with a single function as its only positional argument and
286 no additional keyword arguments, it attaches the mark to the function,
287 containing all the arguments already stored internally in the
288 ``MarkDecorator``.
290 3. When called in any other case, it returns a new ``MarkDecorator``
291 instance with the original ``MarkDecorator``'s content updated with
292 the arguments passed to this call.
294 Note: The rules above prevent a ``MarkDecorator`` from storing only a
295 single function or class reference as its positional argument with no
296 additional keyword or positional arguments. You can work around this by
297 using `with_args()`.
298 """
300 mark: Mark
302 def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
303 """:meta private:"""
304 check_ispytest(_ispytest)
305 self.mark = mark
307 @property
308 def name(self) -> str:
309 """Alias for mark.name."""
310 return self.mark.name
312 @property
313 def args(self) -> Tuple[Any, ...]:
314 """Alias for mark.args."""
315 return self.mark.args
317 @property
318 def kwargs(self) -> Mapping[str, Any]:
319 """Alias for mark.kwargs."""
320 return self.mark.kwargs
322 @property
323 def markname(self) -> str:
324 """:meta private:"""
325 return self.name # for backward-compat (2.4.1 had this attr)
327 def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
328 """Return a MarkDecorator with extra arguments added.
330 Unlike calling the MarkDecorator, with_args() can be used even
331 if the sole argument is a callable/class.
332 """
333 mark = Mark(self.name, args, kwargs, _ispytest=True)
334 return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
336 # Type ignored because the overloads overlap with an incompatible
337 # return type. Not much we can do about that. Thankfully mypy picks
338 # the first match so it works out even if we break the rules.
339 @overload
340 def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
341 pass
343 @overload
344 def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator":
345 pass
347 def __call__(self, *args: object, **kwargs: object):
348 """Call the MarkDecorator."""
349 if args and not kwargs:
350 func = args[0]
351 is_class = inspect.isclass(func)
352 if len(args) == 1 and (istestfunc(func) or is_class):
353 store_mark(func, self.mark)
354 return func
355 return self.with_args(*args, **kwargs)
358def get_unpacked_marks(
359 obj: Union[object, type],
360 *,
361 consider_mro: bool = True,
362) -> List[Mark]:
363 """Obtain the unpacked marks that are stored on an object.
365 If obj is a class and consider_mro is true, return marks applied to
366 this class and all of its super-classes in MRO order. If consider_mro
367 is false, only return marks applied directly to this class.
368 """
369 if isinstance(obj, type):
370 if not consider_mro:
371 mark_lists = [obj.__dict__.get("pytestmark", [])]
372 else:
373 mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
374 mark_list = []
375 for item in mark_lists:
376 if isinstance(item, list):
377 mark_list.extend(item)
378 else:
379 mark_list.append(item)
380 else:
381 mark_attribute = getattr(obj, "pytestmark", [])
382 if isinstance(mark_attribute, list):
383 mark_list = mark_attribute
384 else:
385 mark_list = [mark_attribute]
386 return list(normalize_mark_list(mark_list))
389def normalize_mark_list(
390 mark_list: Iterable[Union[Mark, MarkDecorator]]
391) -> Iterable[Mark]:
392 """
393 Normalize an iterable of Mark or MarkDecorator objects into a list of marks
394 by retrieving the `mark` attribute on MarkDecorator instances.
396 :param mark_list: marks to normalize
397 :returns: A new list of the extracted Mark objects
398 """
399 for mark in mark_list:
400 mark_obj = getattr(mark, "mark", mark)
401 if not isinstance(mark_obj, Mark):
402 raise TypeError(f"got {repr(mark_obj)} instead of Mark")
403 yield mark_obj
406def store_mark(obj, mark: Mark) -> None:
407 """Store a Mark on an object.
409 This is used to implement the Mark declarations/decorators correctly.
410 """
411 assert isinstance(mark, Mark), mark
412 # Always reassign name to avoid updating pytestmark in a reference that
413 # was only borrowed.
414 obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
417# Typing for builtin pytest marks. This is cheating; it gives builtin marks
418# special privilege, and breaks modularity. But practicality beats purity...
419if TYPE_CHECKING:
420 from _pytest.scope import _ScopeName
422 class _SkipMarkDecorator(MarkDecorator):
423 @overload # type: ignore[override,misc,no-overload-impl]
424 def __call__(self, arg: Markable) -> Markable:
425 ...
427 @overload
428 def __call__(self, reason: str = ...) -> "MarkDecorator":
429 ...
431 class _SkipifMarkDecorator(MarkDecorator):
432 def __call__( # type: ignore[override]
433 self,
434 condition: Union[str, bool] = ...,
435 *conditions: Union[str, bool],
436 reason: str = ...,
437 ) -> MarkDecorator:
438 ...
440 class _XfailMarkDecorator(MarkDecorator):
441 @overload # type: ignore[override,misc,no-overload-impl]
442 def __call__(self, arg: Markable) -> Markable:
443 ...
445 @overload
446 def __call__(
447 self,
448 condition: Union[str, bool] = ...,
449 *conditions: Union[str, bool],
450 reason: str = ...,
451 run: bool = ...,
452 raises: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = ...,
453 strict: bool = ...,
454 ) -> MarkDecorator:
455 ...
457 class _ParametrizeMarkDecorator(MarkDecorator):
458 def __call__( # type: ignore[override]
459 self,
460 argnames: Union[str, Sequence[str]],
461 argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
462 *,
463 indirect: Union[bool, Sequence[str]] = ...,
464 ids: Optional[
465 Union[
466 Iterable[Union[None, str, float, int, bool]],
467 Callable[[Any], Optional[object]],
468 ]
469 ] = ...,
470 scope: Optional[_ScopeName] = ...,
471 ) -> MarkDecorator:
472 ...
474 class _UsefixturesMarkDecorator(MarkDecorator):
475 def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
476 ...
478 class _FilterwarningsMarkDecorator(MarkDecorator):
479 def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]
480 ...
483@final
484class MarkGenerator:
485 """Factory for :class:`MarkDecorator` objects - exposed as
486 a ``pytest.mark`` singleton instance.
488 Example::
490 import pytest
492 @pytest.mark.slowtest
493 def test_function():
494 pass
496 applies a 'slowtest' :class:`Mark` on ``test_function``.
497 """
499 # See TYPE_CHECKING above.
500 if TYPE_CHECKING:
501 skip: _SkipMarkDecorator
502 skipif: _SkipifMarkDecorator
503 xfail: _XfailMarkDecorator
504 parametrize: _ParametrizeMarkDecorator
505 usefixtures: _UsefixturesMarkDecorator
506 filterwarnings: _FilterwarningsMarkDecorator
508 def __init__(self, *, _ispytest: bool = False) -> None:
509 check_ispytest(_ispytest)
510 self._config: Optional[Config] = None
511 self._markers: Set[str] = set()
513 def __getattr__(self, name: str) -> MarkDecorator:
514 """Generate a new :class:`MarkDecorator` with the given name."""
515 if name[0] == "_":
516 raise AttributeError("Marker name must NOT start with underscore")
518 if self._config is not None:
519 # We store a set of markers as a performance optimisation - if a mark
520 # name is in the set we definitely know it, but a mark may be known and
521 # not in the set. We therefore start by updating the set!
522 if name not in self._markers:
523 for line in self._config.getini("markers"):
524 # example lines: "skipif(condition): skip the given test if..."
525 # or "hypothesis: tests which use Hypothesis", so to get the
526 # marker name we split on both `:` and `(`.
527 marker = line.split(":")[0].split("(")[0].strip()
528 self._markers.add(marker)
530 # If the name is not in the set of known marks after updating,
531 # then it really is time to issue a warning or an error.
532 if name not in self._markers:
533 if self._config.option.strict_markers or self._config.option.strict:
534 fail(
535 f"{name!r} not found in `markers` configuration option",
536 pytrace=False,
537 )
539 # Raise a specific error for common misspellings of "parametrize".
540 if name in ["parameterize", "parametrise", "parameterise"]:
541 __tracebackhide__ = True
542 fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
544 warnings.warn(
545 "Unknown pytest.mark.%s - is this a typo? You can register "
546 "custom marks to avoid this warning - for details, see "
547 "https://docs.pytest.org/en/stable/how-to/mark.html" % name,
548 PytestUnknownMarkWarning,
549 2,
550 )
552 return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
555MARK_GEN = MarkGenerator(_ispytest=True)
558@final
559class NodeKeywords(MutableMapping[str, Any]):
560 __slots__ = ("node", "parent", "_markers")
562 def __init__(self, node: "Node") -> None:
563 self.node = node
564 self.parent = node.parent
565 self._markers = {node.name: True}
567 def __getitem__(self, key: str) -> Any:
568 try:
569 return self._markers[key]
570 except KeyError:
571 if self.parent is None:
572 raise
573 return self.parent.keywords[key]
575 def __setitem__(self, key: str, value: Any) -> None:
576 self._markers[key] = value
578 # Note: we could've avoided explicitly implementing some of the methods
579 # below and use the collections.abc fallback, but that would be slow.
581 def __contains__(self, key: object) -> bool:
582 return (
583 key in self._markers
584 or self.parent is not None
585 and key in self.parent.keywords
586 )
588 def update( # type: ignore[override]
589 self,
590 other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (),
591 **kwds: Any,
592 ) -> None:
593 self._markers.update(other)
594 self._markers.update(kwds)
596 def __delitem__(self, key: str) -> None:
597 raise ValueError("cannot delete key in keywords dict")
599 def __iter__(self) -> Iterator[str]:
600 # Doesn't need to be fast.
601 yield from self._markers
602 if self.parent is not None:
603 for keyword in self.parent.keywords:
604 # self._marks and self.parent.keywords can have duplicates.
605 if keyword not in self._markers:
606 yield keyword
608 def __len__(self) -> int:
609 # Doesn't need to be fast.
610 return sum(1 for keyword in self)
612 def __repr__(self) -> str:
613 return f"<NodeKeywords for node {self.node}>"