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 `. + +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 ` object to the collected :class:`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",