Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/_pytest/mark/structures.py : 21%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import collections.abc
2import inspect
3import typing
4import warnings
5from typing import Any
6from typing import Callable
7from typing import Iterable
8from typing import List
9from typing import Mapping
10from typing import NamedTuple
11from typing import Optional
12from typing import Sequence
13from typing import Set
14from typing import Tuple
15from typing import TypeVar
16from typing import Union
18import attr
20from .._code import getfslineno
21from ..compat import ascii_escaped
22from ..compat import NOTSET
23from ..compat import NotSetType
24from ..compat import overload
25from ..compat import TYPE_CHECKING
26from _pytest.config import Config
27from _pytest.outcomes import fail
28from _pytest.warning_types import PytestUnknownMarkWarning
30if TYPE_CHECKING:
31 from typing import Type
34EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
37def istestfunc(func) -> bool:
38 return (
39 hasattr(func, "__call__")
40 and getattr(func, "__name__", "<lambda>") != "<lambda>"
41 )
44def get_empty_parameterset_mark(
45 config: Config, argnames: Sequence[str], func
46) -> "MarkDecorator":
47 from ..nodes import Collector
49 fs, lineno = getfslineno(func)
50 reason = "got empty parameter set %r, function %s at %s:%d" % (
51 argnames,
52 func.__name__,
53 fs,
54 lineno,
55 )
57 requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
58 if requested_mark in ("", None, "skip"):
59 mark = MARK_GEN.skip(reason=reason)
60 elif requested_mark == "xfail":
61 mark = MARK_GEN.xfail(reason=reason, run=False)
62 elif requested_mark == "fail_at_collect":
63 f_name = func.__name__
64 _, lineno = getfslineno(func)
65 raise Collector.CollectError(
66 "Empty parameter set in '%s' at line %d" % (f_name, lineno + 1)
67 )
68 else:
69 raise LookupError(requested_mark)
70 return mark
73class ParameterSet(
74 NamedTuple(
75 "ParameterSet",
76 [
77 ("values", Sequence[Union[object, NotSetType]]),
78 ("marks", "typing.Collection[Union[MarkDecorator, Mark]]"),
79 ("id", Optional[str]),
80 ],
81 )
82):
83 @classmethod
84 def param(
85 cls,
86 *values: object,
87 marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
88 id: Optional[str] = None
89 ) -> "ParameterSet":
90 if isinstance(marks, MarkDecorator):
91 marks = (marks,)
92 else:
93 # TODO(py36): Change to collections.abc.Collection.
94 assert isinstance(marks, (collections.abc.Sequence, set))
96 if id is not None:
97 if not isinstance(id, str):
98 raise TypeError(
99 "Expected id to be a string, got {}: {!r}".format(type(id), id)
100 )
101 id = ascii_escaped(id)
102 return cls(values, marks, id)
104 @classmethod
105 def extract_from(
106 cls,
107 parameterset: Union["ParameterSet", Sequence[object], object],
108 force_tuple: bool = False,
109 ) -> "ParameterSet":
110 """
111 :param parameterset:
112 a legacy style parameterset that may or may not be a tuple,
113 and may or may not be wrapped into a mess of mark objects
115 :param force_tuple:
116 enforce tuple wrapping so single argument tuple values
117 don't get decomposed and break tests
118 """
120 if isinstance(parameterset, cls):
121 return parameterset
122 if force_tuple:
123 return cls.param(parameterset)
124 else:
125 # TODO: Refactor to fix this type-ignore. Currently the following
126 # type-checks but crashes:
127 #
128 # @pytest.mark.parametrize(('x', 'y'), [1, 2])
129 # def test_foo(x, y): pass
130 return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
132 @staticmethod
133 def _parse_parametrize_args(
134 argnames: Union[str, List[str], Tuple[str, ...]],
135 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
136 *args,
137 **kwargs
138 ) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
139 if not isinstance(argnames, (tuple, list)):
140 argnames = [x.strip() for x in argnames.split(",") if x.strip()]
141 force_tuple = len(argnames) == 1
142 else:
143 force_tuple = False
144 return argnames, force_tuple
146 @staticmethod
147 def _parse_parametrize_parameters(
148 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
149 force_tuple: bool,
150 ) -> List["ParameterSet"]:
151 return [
152 ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
153 ]
155 @classmethod
156 def _for_parametrize(
157 cls,
158 argnames: Union[str, List[str], Tuple[str, ...]],
159 argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
160 func,
161 config: Config,
162 nodeid: str,
163 ) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]:
164 argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
165 parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
166 del argvalues
168 if parameters:
169 # check all parameter sets have the correct number of values
170 for param in parameters:
171 if len(param.values) != len(argnames):
172 msg = (
173 '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
174 " {names}\n"
175 "must be equal to the number of values ({values_len}):\n"
176 " {values}"
177 )
178 fail(
179 msg.format(
180 nodeid=nodeid,
181 values=param.values,
182 names=argnames,
183 names_len=len(argnames),
184 values_len=len(param.values),
185 ),
186 pytrace=False,
187 )
188 else:
189 # empty parameter set (likely computed at runtime): create a single
190 # parameter set with NOTSET values, with the "empty parameter set" mark applied to it
191 mark = get_empty_parameterset_mark(config, argnames, func)
192 parameters.append(
193 ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None)
194 )
195 return argnames, parameters
198@attr.s(frozen=True)
199class Mark:
200 #: Name of the mark.
201 name = attr.ib(type=str)
202 #: Positional arguments of the mark decorator.
203 args = attr.ib(type=Tuple[Any, ...])
204 #: Keyword arguments of the mark decorator.
205 kwargs = attr.ib(type=Mapping[str, Any])
207 #: Source Mark for ids with parametrize Marks.
208 _param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False)
209 #: Resolved/generated ids with parametrize Marks.
210 _param_ids_generated = attr.ib(
211 type=Optional[Sequence[str]], default=None, repr=False
212 )
214 def _has_param_ids(self) -> bool:
215 return "ids" in self.kwargs or len(self.args) >= 4
217 def combined_with(self, other: "Mark") -> "Mark":
218 """Return a new Mark which is a combination of this
219 Mark and another Mark.
221 Combines by appending args and merging kwargs.
223 :param other: The mark to combine with.
224 :type other: Mark
225 :rtype: Mark
226 """
227 assert self.name == other.name
229 # Remember source of ids with parametrize Marks.
230 param_ids_from = None # type: Optional[Mark]
231 if self.name == "parametrize":
232 if other._has_param_ids():
233 param_ids_from = other
234 elif self._has_param_ids():
235 param_ids_from = self
237 return Mark(
238 self.name,
239 self.args + other.args,
240 dict(self.kwargs, **other.kwargs),
241 param_ids_from=param_ids_from,
242 )
245# A generic parameter designating an object to which a Mark may
246# be applied -- a test function (callable) or class.
247# Note: a lambda is not allowed, but this can't be represented.
248_Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type])
251@attr.s
252class MarkDecorator:
253 """A decorator for applying a mark on test functions and classes.
255 MarkDecorators are created with ``pytest.mark``::
257 mark1 = pytest.mark.NAME # Simple MarkDecorator
258 mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
260 and can then be applied as decorators to test functions::
262 @mark2
263 def test_function():
264 pass
266 When a MarkDecorator is called it does the following:
268 1. If called with a single class as its only positional argument and no
269 additional keyword arguments, it attaches the mark to the class so it
270 gets applied automatically to all test cases found in that class.
272 2. If called with a single function as its only positional argument and
273 no additional keyword arguments, it attaches the mark to the function,
274 containing all the arguments already stored internally in the
275 MarkDecorator.
277 3. When called in any other case, it returns a new MarkDecorator instance
278 with the original MarkDecorator's content updated with the arguments
279 passed to this call.
281 Note: The rules above prevent MarkDecorators from storing only a single
282 function or class reference as their positional argument with no
283 additional keyword or positional arguments. You can work around this by
284 using `with_args()`.
285 """
287 mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark))
289 @property
290 def name(self) -> str:
291 """Alias for mark.name."""
292 return self.mark.name
294 @property
295 def args(self) -> Tuple[Any, ...]:
296 """Alias for mark.args."""
297 return self.mark.args
299 @property
300 def kwargs(self) -> Mapping[str, Any]:
301 """Alias for mark.kwargs."""
302 return self.mark.kwargs
304 @property
305 def markname(self) -> str:
306 return self.name # for backward-compat (2.4.1 had this attr)
308 def __repr__(self) -> str:
309 return "<MarkDecorator {!r}>".format(self.mark)
311 def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
312 """Return a MarkDecorator with extra arguments added.
314 Unlike calling the MarkDecorator, with_args() can be used even
315 if the sole argument is a callable/class.
317 :return: MarkDecorator
318 """
319 mark = Mark(self.name, args, kwargs)
320 return self.__class__(self.mark.combined_with(mark))
322 # Type ignored because the overloads overlap with an incompatible
323 # return type. Not much we can do about that. Thankfully mypy picks
324 # the first match so it works out even if we break the rules.
325 @overload
326 def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc]
327 raise NotImplementedError()
329 @overload # noqa: F811
330 def __call__( # noqa: F811
331 self, *args: object, **kwargs: object
332 ) -> "MarkDecorator":
333 raise NotImplementedError()
335 def __call__(self, *args: object, **kwargs: object): # noqa: F811
336 """Call the MarkDecorator."""
337 if args and not kwargs:
338 func = args[0]
339 is_class = inspect.isclass(func)
340 if len(args) == 1 and (istestfunc(func) or is_class):
341 store_mark(func, self.mark)
342 return func
343 return self.with_args(*args, **kwargs)
346def get_unpacked_marks(obj) -> List[Mark]:
347 """
348 obtain the unpacked marks that are stored on an object
349 """
350 mark_list = getattr(obj, "pytestmark", [])
351 if not isinstance(mark_list, list):
352 mark_list = [mark_list]
353 return normalize_mark_list(mark_list)
356def normalize_mark_list(mark_list: Iterable[Union[Mark, MarkDecorator]]) -> List[Mark]:
357 """
358 normalizes marker decorating helpers to mark objects
360 :type mark_list: List[Union[Mark, Markdecorator]]
361 :rtype: List[Mark]
362 """
363 extracted = [
364 getattr(mark, "mark", mark) for mark in mark_list
365 ] # unpack MarkDecorator
366 for mark in extracted:
367 if not isinstance(mark, Mark):
368 raise TypeError("got {!r} instead of Mark".format(mark))
369 return [x for x in extracted if isinstance(x, Mark)]
372def store_mark(obj, mark: Mark) -> None:
373 """Store a Mark on an object.
375 This is used to implement the Mark declarations/decorators correctly.
376 """
377 assert isinstance(mark, Mark), mark
378 # Always reassign name to avoid updating pytestmark in a reference that
379 # was only borrowed.
380 obj.pytestmark = get_unpacked_marks(obj) + [mark]
383# Typing for builtin pytest marks. This is cheating; it gives builtin marks
384# special privilege, and breaks modularity. But practicality beats purity...
385if TYPE_CHECKING:
386 from _pytest.fixtures import _Scope
388 class _SkipMarkDecorator(MarkDecorator):
389 @overload # type: ignore[override,misc]
390 def __call__(self, arg: _Markable) -> _Markable:
391 raise NotImplementedError()
393 @overload # noqa: F811
394 def __call__(self, reason: str = ...) -> "MarkDecorator": # noqa: F811
395 raise NotImplementedError()
397 class _SkipifMarkDecorator(MarkDecorator):
398 def __call__( # type: ignore[override]
399 self,
400 condition: Union[str, bool] = ...,
401 *conditions: Union[str, bool],
402 reason: str = ...
403 ) -> MarkDecorator:
404 raise NotImplementedError()
406 class _XfailMarkDecorator(MarkDecorator):
407 @overload # type: ignore[override,misc]
408 def __call__(self, arg: _Markable) -> _Markable:
409 raise NotImplementedError()
411 @overload # noqa: F811
412 def __call__( # noqa: F811
413 self,
414 condition: Union[str, bool] = ...,
415 *conditions: Union[str, bool],
416 reason: str = ...,
417 run: bool = ...,
418 raises: Union[
419 "Type[BaseException]", Tuple["Type[BaseException]", ...]
420 ] = ...,
421 strict: bool = ...
422 ) -> MarkDecorator:
423 raise NotImplementedError()
425 class _ParametrizeMarkDecorator(MarkDecorator):
426 def __call__( # type: ignore[override]
427 self,
428 argnames: Union[str, List[str], Tuple[str, ...]],
429 argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
430 *,
431 indirect: Union[bool, Sequence[str]] = ...,
432 ids: Optional[
433 Union[
434 Iterable[Union[None, str, float, int, bool]],
435 Callable[[Any], Optional[object]],
436 ]
437 ] = ...,
438 scope: Optional[_Scope] = ...
439 ) -> MarkDecorator:
440 raise NotImplementedError()
442 class _UsefixturesMarkDecorator(MarkDecorator):
443 def __call__( # type: ignore[override]
444 self, *fixtures: str
445 ) -> MarkDecorator:
446 raise NotImplementedError()
448 class _FilterwarningsMarkDecorator(MarkDecorator):
449 def __call__( # type: ignore[override]
450 self, *filters: str
451 ) -> MarkDecorator:
452 raise NotImplementedError()
455class MarkGenerator:
456 """Factory for :class:`MarkDecorator` objects - exposed as
457 a ``pytest.mark`` singleton instance.
459 Example::
461 import pytest
463 @pytest.mark.slowtest
464 def test_function():
465 pass
467 applies a 'slowtest' :class:`Mark` on ``test_function``.
468 """
470 _config = None # type: Optional[Config]
471 _markers = set() # type: Set[str]
473 # See TYPE_CHECKING above.
474 if TYPE_CHECKING:
475 # TODO(py36): Change to builtin annotation syntax.
476 skip = _SkipMarkDecorator(Mark("skip", (), {}))
477 skipif = _SkipifMarkDecorator(Mark("skipif", (), {}))
478 xfail = _XfailMarkDecorator(Mark("xfail", (), {}))
479 parametrize = _ParametrizeMarkDecorator(Mark("parametrize", (), {}))
480 usefixtures = _UsefixturesMarkDecorator(Mark("usefixtures", (), {}))
481 filterwarnings = _FilterwarningsMarkDecorator(Mark("filterwarnings", (), {}))
483 def __getattr__(self, name: str) -> MarkDecorator:
484 if name[0] == "_":
485 raise AttributeError("Marker name must NOT start with underscore")
487 if self._config is not None:
488 # We store a set of markers as a performance optimisation - if a mark
489 # name is in the set we definitely know it, but a mark may be known and
490 # not in the set. We therefore start by updating the set!
491 if name not in self._markers:
492 for line in self._config.getini("markers"):
493 # example lines: "skipif(condition): skip the given test if..."
494 # or "hypothesis: tests which use Hypothesis", so to get the
495 # marker name we split on both `:` and `(`.
496 marker = line.split(":")[0].split("(")[0].strip()
497 self._markers.add(marker)
499 # If the name is not in the set of known marks after updating,
500 # then it really is time to issue a warning or an error.
501 if name not in self._markers:
502 if self._config.option.strict_markers:
503 fail(
504 "{!r} not found in `markers` configuration option".format(name),
505 pytrace=False,
506 )
508 # Raise a specific error for common misspellings of "parametrize".
509 if name in ["parameterize", "parametrise", "parameterise"]:
510 __tracebackhide__ = True
511 fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name))
513 warnings.warn(
514 "Unknown pytest.mark.%s - is this a typo? You can register "
515 "custom marks to avoid this warning - for details, see "
516 "https://docs.pytest.org/en/stable/mark.html" % name,
517 PytestUnknownMarkWarning,
518 2,
519 )
521 return MarkDecorator(Mark(name, (), {}))
524MARK_GEN = MarkGenerator()
527class NodeKeywords(collections.abc.MutableMapping):
528 def __init__(self, node):
529 self.node = node
530 self.parent = node.parent
531 self._markers = {node.name: True}
533 def __getitem__(self, key):
534 try:
535 return self._markers[key]
536 except KeyError:
537 if self.parent is None:
538 raise
539 return self.parent.keywords[key]
541 def __setitem__(self, key, value):
542 self._markers[key] = value
544 def __delitem__(self, key):
545 raise ValueError("cannot delete key in keywords dict")
547 def __iter__(self):
548 seen = self._seen()
549 return iter(seen)
551 def _seen(self):
552 seen = set(self._markers)
553 if self.parent is not None:
554 seen.update(self.parent.keywords)
555 return seen
557 def __len__(self) -> int:
558 return len(self._seen())
560 def __repr__(self) -> str:
561 return "<NodeKeywords for node {}>".format(self.node)