Merge pull request #8179 from bluetech/typing-public-mark
mark: expose Mark, MarkDecorator, MarkGenerator under pytest for typing purposes
This commit is contained in:
commit
813ce45985
|
@ -0,0 +1,7 @@
|
||||||
|
Directly constructing the following classes is now deprecated:
|
||||||
|
|
||||||
|
- ``_pytest.mark.structures.Mark``
|
||||||
|
- ``_pytest.mark.structures.MarkDecorator``
|
||||||
|
- ``_pytest.mark.structures.MarkGenerator``
|
||||||
|
|
||||||
|
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.
|
|
@ -0,0 +1,12 @@
|
||||||
|
The types of objects used in pytest's API are now exported so they may be used in type annotations.
|
||||||
|
|
||||||
|
The newly-exported types are:
|
||||||
|
|
||||||
|
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
|
||||||
|
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
|
||||||
|
- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
|
||||||
|
|
||||||
|
Constructing them directly is not supported; they are only meant for use in type annotations.
|
||||||
|
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
|
||||||
|
|
||||||
|
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
|
|
@ -239,7 +239,7 @@ For example:
|
||||||
def test_function():
|
def test_function():
|
||||||
...
|
...
|
||||||
|
|
||||||
Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
|
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
|
||||||
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
|
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
|
||||||
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
|
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
|
||||||
|
|
||||||
|
@ -849,21 +849,21 @@ Item
|
||||||
MarkDecorator
|
MarkDecorator
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: _pytest.mark.MarkDecorator
|
.. autoclass:: pytest.MarkDecorator()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
MarkGenerator
|
MarkGenerator
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
.. autoclass:: _pytest.mark.MarkGenerator
|
.. autoclass:: pytest.MarkGenerator()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Mark
|
Mark
|
||||||
~~~~
|
~~~~
|
||||||
|
|
||||||
.. autoclass:: _pytest.mark.structures.Mark
|
.. autoclass:: pytest.Mark()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -551,7 +551,7 @@ class FixtureRequest:
|
||||||
on all function invocations.
|
on all function invocations.
|
||||||
|
|
||||||
:param marker:
|
:param marker:
|
||||||
A :py:class:`_pytest.mark.MarkDecorator` object created by a call
|
A :class:`pytest.MarkDecorator` object created by a call
|
||||||
to ``pytest.mark.NAME(...)``.
|
to ``pytest.mark.NAME(...)``.
|
||||||
"""
|
"""
|
||||||
self.node.add_marker(marker)
|
self.node.add_marker(marker)
|
||||||
|
|
|
@ -28,6 +28,7 @@ from ..compat import final
|
||||||
from ..compat import NOTSET
|
from ..compat import NOTSET
|
||||||
from ..compat import NotSetType
|
from ..compat import NotSetType
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.warning_types import PytestUnknownMarkWarning
|
from _pytest.warning_types import PytestUnknownMarkWarning
|
||||||
|
|
||||||
|
@ -200,21 +201,38 @@ class ParameterSet(
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@attr.s(frozen=True)
|
@attr.s(frozen=True, init=False, auto_attribs=True)
|
||||||
class Mark:
|
class Mark:
|
||||||
#: Name of the mark.
|
#: Name of the mark.
|
||||||
name = attr.ib(type=str)
|
name: str
|
||||||
#: Positional arguments of the mark decorator.
|
#: Positional arguments of the mark decorator.
|
||||||
args = attr.ib(type=Tuple[Any, ...])
|
args: Tuple[Any, ...]
|
||||||
#: Keyword arguments of the mark decorator.
|
#: Keyword arguments of the mark decorator.
|
||||||
kwargs = attr.ib(type=Mapping[str, Any])
|
kwargs: Mapping[str, Any]
|
||||||
|
|
||||||
#: Source Mark for ids with parametrize Marks.
|
#: Source Mark for ids with parametrize Marks.
|
||||||
_param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False)
|
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
|
||||||
#: Resolved/generated ids with parametrize Marks.
|
#: Resolved/generated ids with parametrize Marks.
|
||||||
_param_ids_generated = attr.ib(
|
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
|
||||||
type=Optional[Sequence[str]], default=None, repr=False
|
|
||||||
)
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
args: Tuple[Any, ...],
|
||||||
|
kwargs: Mapping[str, Any],
|
||||||
|
param_ids_from: Optional["Mark"] = None,
|
||||||
|
param_ids_generated: Optional[Sequence[str]] = None,
|
||||||
|
*,
|
||||||
|
_ispytest: bool = False,
|
||||||
|
) -> None:
|
||||||
|
""":meta private:"""
|
||||||
|
check_ispytest(_ispytest)
|
||||||
|
# Weirdness to bypass frozen=True.
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "args", args)
|
||||||
|
object.__setattr__(self, "kwargs", kwargs)
|
||||||
|
object.__setattr__(self, "_param_ids_from", param_ids_from)
|
||||||
|
object.__setattr__(self, "_param_ids_generated", param_ids_generated)
|
||||||
|
|
||||||
def _has_param_ids(self) -> bool:
|
def _has_param_ids(self) -> bool:
|
||||||
return "ids" in self.kwargs or len(self.args) >= 4
|
return "ids" in self.kwargs or len(self.args) >= 4
|
||||||
|
@ -243,20 +261,21 @@ class Mark:
|
||||||
self.args + other.args,
|
self.args + other.args,
|
||||||
dict(self.kwargs, **other.kwargs),
|
dict(self.kwargs, **other.kwargs),
|
||||||
param_ids_from=param_ids_from,
|
param_ids_from=param_ids_from,
|
||||||
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# A generic parameter designating an object to which a Mark may
|
# A generic parameter designating an object to which a Mark may
|
||||||
# be applied -- a test function (callable) or class.
|
# be applied -- a test function (callable) or class.
|
||||||
# Note: a lambda is not allowed, but this can't be represented.
|
# Note: a lambda is not allowed, but this can't be represented.
|
||||||
_Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type])
|
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s(init=False, auto_attribs=True)
|
||||||
class MarkDecorator:
|
class MarkDecorator:
|
||||||
"""A decorator for applying a mark on test functions and classes.
|
"""A decorator for applying a mark on test functions and classes.
|
||||||
|
|
||||||
MarkDecorators are created with ``pytest.mark``::
|
``MarkDecorators`` are created with ``pytest.mark``::
|
||||||
|
|
||||||
mark1 = pytest.mark.NAME # Simple MarkDecorator
|
mark1 = pytest.mark.NAME # Simple MarkDecorator
|
||||||
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
|
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
|
||||||
|
@ -267,7 +286,7 @@ class MarkDecorator:
|
||||||
def test_function():
|
def test_function():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
When a MarkDecorator is called it does the following:
|
When a ``MarkDecorator`` is called, it does the following:
|
||||||
|
|
||||||
1. If called with a single class as its only positional argument and no
|
1. If called with a single class as its only positional argument and no
|
||||||
additional keyword arguments, it attaches the mark to the class so it
|
additional keyword arguments, it attaches the mark to the class so it
|
||||||
|
@ -276,19 +295,24 @@ class MarkDecorator:
|
||||||
2. If called with a single function as its only positional argument and
|
2. If called with a single function as its only positional argument and
|
||||||
no additional keyword arguments, it attaches the mark to the function,
|
no additional keyword arguments, it attaches the mark to the function,
|
||||||
containing all the arguments already stored internally in the
|
containing all the arguments already stored internally in the
|
||||||
MarkDecorator.
|
``MarkDecorator``.
|
||||||
|
|
||||||
3. When called in any other case, it returns a new MarkDecorator instance
|
3. When called in any other case, it returns a new ``MarkDecorator``
|
||||||
with the original MarkDecorator's content updated with the arguments
|
instance with the original ``MarkDecorator``'s content updated with
|
||||||
passed to this call.
|
the arguments passed to this call.
|
||||||
|
|
||||||
Note: The rules above prevent MarkDecorators from storing only a single
|
Note: The rules above prevent a ``MarkDecorator`` from storing only a
|
||||||
function or class reference as their positional argument with no
|
single function or class reference as its positional argument with no
|
||||||
additional keyword or positional arguments. You can work around this by
|
additional keyword or positional arguments. You can work around this by
|
||||||
using `with_args()`.
|
using `with_args()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark))
|
mark: Mark
|
||||||
|
|
||||||
|
def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
|
||||||
|
""":meta private:"""
|
||||||
|
check_ispytest(_ispytest)
|
||||||
|
self.mark = mark
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
|
@ -307,6 +331,7 @@ class MarkDecorator:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def markname(self) -> str:
|
def markname(self) -> str:
|
||||||
|
""":meta private:"""
|
||||||
return self.name # for backward-compat (2.4.1 had this attr)
|
return self.name # for backward-compat (2.4.1 had this attr)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -317,17 +342,15 @@ class MarkDecorator:
|
||||||
|
|
||||||
Unlike calling the MarkDecorator, with_args() can be used even
|
Unlike calling the MarkDecorator, with_args() can be used even
|
||||||
if the sole argument is a callable/class.
|
if the sole argument is a callable/class.
|
||||||
|
|
||||||
:rtype: MarkDecorator
|
|
||||||
"""
|
"""
|
||||||
mark = Mark(self.name, args, kwargs)
|
mark = Mark(self.name, args, kwargs, _ispytest=True)
|
||||||
return self.__class__(self.mark.combined_with(mark))
|
return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
|
||||||
|
|
||||||
# Type ignored because the overloads overlap with an incompatible
|
# Type ignored because the overloads overlap with an incompatible
|
||||||
# return type. Not much we can do about that. Thankfully mypy picks
|
# 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.
|
# the first match so it works out even if we break the rules.
|
||||||
@overload
|
@overload
|
||||||
def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc]
|
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -386,7 +409,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class _SkipMarkDecorator(MarkDecorator):
|
class _SkipMarkDecorator(MarkDecorator):
|
||||||
@overload # type: ignore[override,misc]
|
@overload # type: ignore[override,misc]
|
||||||
def __call__(self, arg: _Markable) -> _Markable:
|
def __call__(self, arg: Markable) -> Markable:
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -404,7 +427,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
class _XfailMarkDecorator(MarkDecorator):
|
class _XfailMarkDecorator(MarkDecorator):
|
||||||
@overload # type: ignore[override,misc]
|
@overload # type: ignore[override,misc]
|
||||||
def __call__(self, arg: _Markable) -> _Markable:
|
def __call__(self, arg: Markable) -> Markable:
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -465,9 +488,6 @@ class MarkGenerator:
|
||||||
applies a 'slowtest' :class:`Mark` on ``test_function``.
|
applies a 'slowtest' :class:`Mark` on ``test_function``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_config: Optional[Config] = None
|
|
||||||
_markers: Set[str] = set()
|
|
||||||
|
|
||||||
# See TYPE_CHECKING above.
|
# See TYPE_CHECKING above.
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
skip: _SkipMarkDecorator
|
skip: _SkipMarkDecorator
|
||||||
|
@ -477,7 +497,13 @@ class MarkGenerator:
|
||||||
usefixtures: _UsefixturesMarkDecorator
|
usefixtures: _UsefixturesMarkDecorator
|
||||||
filterwarnings: _FilterwarningsMarkDecorator
|
filterwarnings: _FilterwarningsMarkDecorator
|
||||||
|
|
||||||
|
def __init__(self, *, _ispytest: bool = False) -> None:
|
||||||
|
check_ispytest(_ispytest)
|
||||||
|
self._config: Optional[Config] = None
|
||||||
|
self._markers: Set[str] = set()
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> MarkDecorator:
|
def __getattr__(self, name: str) -> MarkDecorator:
|
||||||
|
"""Generate a new :class:`MarkDecorator` with the given name."""
|
||||||
if name[0] == "_":
|
if name[0] == "_":
|
||||||
raise AttributeError("Marker name must NOT start with underscore")
|
raise AttributeError("Marker name must NOT start with underscore")
|
||||||
|
|
||||||
|
@ -515,10 +541,10 @@ class MarkGenerator:
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
|
||||||
return MarkDecorator(Mark(name, (), {}))
|
return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
|
||||||
|
|
||||||
|
|
||||||
MARK_GEN = MarkGenerator()
|
MARK_GEN = MarkGenerator(_ispytest=True)
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
|
@ -21,7 +21,10 @@ from _pytest.fixtures import yield_fixture
|
||||||
from _pytest.freeze_support import freeze_includes
|
from _pytest.freeze_support import freeze_includes
|
||||||
from _pytest.logging import LogCaptureFixture
|
from _pytest.logging import LogCaptureFixture
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
|
from _pytest.mark import Mark
|
||||||
from _pytest.mark import MARK_GEN as mark
|
from _pytest.mark import MARK_GEN as mark
|
||||||
|
from _pytest.mark import MarkDecorator
|
||||||
|
from _pytest.mark import MarkGenerator
|
||||||
from _pytest.mark import param
|
from _pytest.mark import param
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.nodes import Collector
|
from _pytest.nodes import Collector
|
||||||
|
@ -89,6 +92,9 @@ __all__ = [
|
||||||
"LogCaptureFixture",
|
"LogCaptureFixture",
|
||||||
"main",
|
"main",
|
||||||
"mark",
|
"mark",
|
||||||
|
"Mark",
|
||||||
|
"MarkDecorator",
|
||||||
|
"MarkGenerator",
|
||||||
"Module",
|
"Module",
|
||||||
"MonkeyPatch",
|
"MonkeyPatch",
|
||||||
"Package",
|
"Package",
|
||||||
|
|
|
@ -21,7 +21,7 @@ class TestMark:
|
||||||
assert attr in module.__all__ # type: ignore
|
assert attr in module.__all__ # type: ignore
|
||||||
|
|
||||||
def test_pytest_mark_notcallable(self) -> None:
|
def test_pytest_mark_notcallable(self) -> None:
|
||||||
mark = MarkGenerator()
|
mark = MarkGenerator(_ispytest=True)
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
mark() # type: ignore[operator]
|
mark() # type: ignore[operator]
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class TestMark:
|
||||||
assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap]
|
assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap]
|
||||||
|
|
||||||
def test_pytest_mark_name_starts_with_underscore(self) -> None:
|
def test_pytest_mark_name_starts_with_underscore(self) -> None:
|
||||||
mark = MarkGenerator()
|
mark = MarkGenerator(_ispytest=True)
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
mark._some_name
|
mark._some_name
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue