From 2ec372df8b987207efc4ad0f33c2f82df5c9e2e5 Mon Sep 17 00:00:00 2001 From: Ran Benita <ran@unusedvar.com> Date: Sat, 19 Dec 2020 22:19:51 +0200 Subject: [PATCH 1/3] mark: export pytest.Mark for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 5 +++++ changelog/7469.feature.rst | 10 +++++++++ doc/en/reference.rst | 4 ++-- src/_pytest/mark/structures.py | 39 +++++++++++++++++++++++++--------- src/pytest/__init__.py | 2 ++ 5 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 changelog/7469.deprecation.rst create mode 100644 changelog/7469.feature.rst diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst new file mode 100644 index 000000000..79419dace --- /dev/null +++ b/changelog/7469.deprecation.rst @@ -0,0 +1,5 @@ +Directly constructing the following classes is now deprecated: + +- ``_pytest.mark.structures.Mark`` + +These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst new file mode 100644 index 000000000..f4bc52414 --- /dev/null +++ b/changelog/7469.feature.rst @@ -0,0 +1,10 @@ +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>`. + +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. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 8aa95ca64..3fc62ee72 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -239,7 +239,7 @@ For example: 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 :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes: @@ -863,7 +863,7 @@ MarkGenerator Mark ~~~~ -.. autoclass:: _pytest.mark.structures.Mark +.. autoclass:: pytest.Mark() :members: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 6c126cf4a..29b958687 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -28,6 +28,7 @@ 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.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning @@ -200,21 +201,38 @@ class ParameterSet( @final -@attr.s(frozen=True) +@attr.s(frozen=True, init=False, auto_attribs=True) class Mark: #: Name of the mark. - name = attr.ib(type=str) + name: str #: Positional arguments of the mark decorator. - args = attr.ib(type=Tuple[Any, ...]) + args: Tuple[Any, ...] #: Keyword arguments of the mark decorator. - kwargs = attr.ib(type=Mapping[str, Any]) + kwargs: Mapping[str, Any] #: 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. - _param_ids_generated = attr.ib( - type=Optional[Sequence[str]], default=None, repr=False - ) + _param_ids_generated: Optional[Sequence[str]] = attr.ib(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: return "ids" in self.kwargs or len(self.args) >= 4 @@ -243,6 +261,7 @@ class Mark: self.args + other.args, dict(self.kwargs, **other.kwargs), param_ids_from=param_ids_from, + _ispytest=True, ) @@ -320,7 +339,7 @@ class MarkDecorator: :rtype: MarkDecorator """ - mark = Mark(self.name, args, kwargs) + mark = Mark(self.name, args, kwargs, _ispytest=True) return self.__class__(self.mark.combined_with(mark)) # Type ignored because the overloads overlap with an incompatible @@ -515,7 +534,7 @@ class MarkGenerator: 2, ) - return MarkDecorator(Mark(name, (), {})) + return MarkDecorator(Mark(name, (), {}, _ispytest=True)) MARK_GEN = MarkGenerator() diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 70177f950..4b194e0c8 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -21,6 +21,7 @@ from _pytest.fixtures import yield_fixture from _pytest.freeze_support import freeze_includes from _pytest.logging import LogCaptureFixture from _pytest.main import Session +from _pytest.mark import Mark from _pytest.mark import MARK_GEN as mark from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch @@ -89,6 +90,7 @@ __all__ = [ "LogCaptureFixture", "main", "mark", + "Mark", "Module", "MonkeyPatch", "Package", From 69c302479e3f76450f29e7d2de24254d5eda6492 Mon Sep 17 00:00:00 2001 From: Ran Benita <ran@unusedvar.com> Date: Sun, 20 Dec 2020 15:11:01 +0200 Subject: [PATCH 2/3] mark: export pytest.MarkDecorator for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/fixtures.py | 2 +- src/_pytest/mark/structures.py | 40 +++++++++++++++++++--------------- src/pytest/__init__.py | 2 ++ 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index 79419dace..6922b3bbb 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -1,5 +1,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.Mark`` +- ``_pytest.mark.structures.MarkDecorator`` These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst index f4bc52414..66113aa58 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -3,6 +3,7 @@ The types of objects used in pytest's API are now exported so they may be used i The newly-exported types are: - ``pytest.Mark`` for :class:`marks <pytest.Mark>`. +- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`. 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. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 3fc62ee72..8bd4111a1 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -849,7 +849,7 @@ Item MarkDecorator ~~~~~~~~~~~~~ -.. autoclass:: _pytest.mark.MarkDecorator +.. autoclass:: pytest.MarkDecorator() :members: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c24ab7069..dbb039bf2 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -551,7 +551,7 @@ class FixtureRequest: on all function invocations. :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(...)``. """ self.node.add_marker(marker) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 29b958687..8bce33e68 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -268,14 +268,14 @@ 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=Union[Callable[..., object], type]) +Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) -@attr.s +@attr.s(init=False, auto_attribs=True) class MarkDecorator: """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 mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator @@ -286,7 +286,7 @@ class MarkDecorator: def test_function(): 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 additional keyword arguments, it attaches the mark to the class so it @@ -295,19 +295,24 @@ class MarkDecorator: 2. If called with a single function as its only positional argument and no additional keyword arguments, it attaches the mark to the function, containing all the arguments already stored internally in the - MarkDecorator. + ``MarkDecorator``. - 3. When called in any other case, it returns a new MarkDecorator instance - with the original MarkDecorator's content updated with the arguments - passed to this call. + 3. When called in any other case, it returns a new ``MarkDecorator`` + instance with the original ``MarkDecorator``'s content updated with + the arguments passed to this call. - Note: The rules above prevent MarkDecorators from storing only a single - function or class reference as their positional argument with no + Note: The rules above prevent a ``MarkDecorator`` from storing only a + single function or class reference as its positional argument with no additional keyword or positional arguments. You can work around this by 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 def name(self) -> str: @@ -326,6 +331,7 @@ class MarkDecorator: @property def markname(self) -> str: + """:meta private:""" return self.name # for backward-compat (2.4.1 had this attr) def __repr__(self) -> str: @@ -336,17 +342,15 @@ class MarkDecorator: Unlike calling the MarkDecorator, with_args() can be used even if the sole argument is a callable/class. - - :rtype: MarkDecorator """ 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 # 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[misc] + def __call__(self, arg: Markable) -> Markable: # type: ignore[misc] pass @overload @@ -405,7 +409,7 @@ if TYPE_CHECKING: class _SkipMarkDecorator(MarkDecorator): @overload # type: ignore[override,misc] - def __call__(self, arg: _Markable) -> _Markable: + def __call__(self, arg: Markable) -> Markable: ... @overload @@ -423,7 +427,7 @@ if TYPE_CHECKING: class _XfailMarkDecorator(MarkDecorator): @overload # type: ignore[override,misc] - def __call__(self, arg: _Markable) -> _Markable: + def __call__(self, arg: Markable) -> Markable: ... @overload @@ -534,7 +538,7 @@ class MarkGenerator: 2, ) - return MarkDecorator(Mark(name, (), {}, _ispytest=True)) + return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) MARK_GEN = MarkGenerator() diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 4b194e0c8..1d5b38ee0 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -23,6 +23,7 @@ from _pytest.logging import LogCaptureFixture from _pytest.main import Session from _pytest.mark import Mark from _pytest.mark import MARK_GEN as mark +from _pytest.mark import MarkDecorator from _pytest.mark import param from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Collector @@ -91,6 +92,7 @@ __all__ = [ "main", "mark", "Mark", + "MarkDecorator", "Module", "MonkeyPatch", "Package", From 6aa4d1c7ab968aecf44ad89e568a4515bd7e5343 Mon Sep 17 00:00:00 2001 From: Ran Benita <ran@unusedvar.com> Date: Sun, 20 Dec 2020 15:36:24 +0200 Subject: [PATCH 3/3] mark: export pytest.MarkGenerator for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/mark/structures.py | 11 +++++++---- src/pytest/__init__.py | 2 ++ testing/test_mark.py | 4 ++-- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index 6922b3bbb..6bbc80755 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -2,5 +2,6 @@ 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. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst index 66113aa58..81f93d1f7 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -4,6 +4,7 @@ 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. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 8bd4111a1..c8e8dca75 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -856,7 +856,7 @@ MarkDecorator MarkGenerator ~~~~~~~~~~~~~ -.. autoclass:: _pytest.mark.MarkGenerator +.. autoclass:: pytest.MarkGenerator() :members: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 8bce33e68..ae6920735 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -488,9 +488,6 @@ class MarkGenerator: applies a 'slowtest' :class:`Mark` on ``test_function``. """ - _config: Optional[Config] = None - _markers: Set[str] = set() - # See TYPE_CHECKING above. if TYPE_CHECKING: skip: _SkipMarkDecorator @@ -500,7 +497,13 @@ class MarkGenerator: usefixtures: _UsefixturesMarkDecorator 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: + """Generate a new :class:`MarkDecorator` with the given name.""" if name[0] == "_": raise AttributeError("Marker name must NOT start with underscore") @@ -541,7 +544,7 @@ class MarkGenerator: return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) -MARK_GEN = MarkGenerator() +MARK_GEN = MarkGenerator(_ispytest=True) @final diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 1d5b38ee0..74cf00ee2 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -24,6 +24,7 @@ from _pytest.main import Session from _pytest.mark import 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.monkeypatch import MonkeyPatch from _pytest.nodes import Collector @@ -93,6 +94,7 @@ __all__ = [ "mark", "Mark", "MarkDecorator", + "MarkGenerator", "Module", "MonkeyPatch", "Package", diff --git a/testing/test_mark.py b/testing/test_mark.py index e0b91f0ce..5f4b3e063 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -21,7 +21,7 @@ class TestMark: assert attr in module.__all__ # type: ignore def test_pytest_mark_notcallable(self) -> None: - mark = MarkGenerator() + mark = MarkGenerator(_ispytest=True) with pytest.raises(TypeError): mark() # type: ignore[operator] @@ -40,7 +40,7 @@ class TestMark: assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap] def test_pytest_mark_name_starts_with_underscore(self) -> None: - mark = MarkGenerator() + mark = MarkGenerator(_ispytest=True) with pytest.raises(AttributeError): mark._some_name