summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRan Benita <ran@unusedvar.com>2020-12-22 16:32:02 +0200
committerGitHub <noreply@github.com>2020-12-22 16:32:02 +0200
commit813ce45985d50d0c5c99591b224132822d9ae688 (patch)
treed7c7f02e3990311146b1dd47fcc3f997608ff867
parentd64706c33d88406a0aea422530e2392cced2edc2 (diff)
parent6aa4d1c7ab968aecf44ad89e568a4515bd7e5343 (diff)
downloadpytest-813ce45985d50d0c5c99591b224132822d9ae688.tar.gz
Merge pull request #8179 from bluetech/typing-public-mark
mark: expose Mark, MarkDecorator, MarkGenerator under pytest for typing purposes
-rw-r--r--changelog/7469.deprecation.rst7
-rw-r--r--changelog/7469.feature.rst12
-rw-r--r--doc/en/reference.rst8
-rw-r--r--src/_pytest/fixtures.py2
-rw-r--r--src/_pytest/mark/structures.py88
-rw-r--r--src/pytest/__init__.py6
-rw-r--r--testing/test_mark.py4
7 files changed, 89 insertions, 38 deletions
diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst
new file mode 100644
index 000000000..6bbc80755
--- /dev/null
+++ b/changelog/7469.deprecation.rst
@@ -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.
diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst
new file mode 100644
index 000000000..81f93d1f7
--- /dev/null
+++ b/changelog/7469.feature.rst
@@ -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.
diff --git a/doc/en/reference.rst b/doc/en/reference.rst
index 8aa95ca64..c8e8dca75 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:
@@ -849,21 +849,21 @@ Item
MarkDecorator
~~~~~~~~~~~~~
-.. autoclass:: _pytest.mark.MarkDecorator
+.. autoclass:: pytest.MarkDecorator()
:members:
MarkGenerator
~~~~~~~~~~~~~
-.. autoclass:: _pytest.mark.MarkGenerator
+.. autoclass:: pytest.MarkGenerator()
:members:
Mark
~~~~
-.. autoclass:: _pytest.mark.structures.Mark
+.. autoclass:: pytest.Mark()
: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 6c126cf4a..ae6920735 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,20 +261,21 @@ class Mark:
self.args + other.args,
dict(self.kwargs, **other.kwargs),
param_ids_from=param_ids_from,
+ _ispytest=True,
)
# 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
@@ -267,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
@@ -276,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:
@@ -307,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:
@@ -317,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)
- return self.__class__(self.mark.combined_with(mark))
+ mark = Mark(self.name, args, kwargs, _ispytest=True)
+ 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
@@ -386,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
@@ -404,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
@@ -465,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
@@ -477,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")
@@ -515,10 +541,10 @@ class MarkGenerator:
2,
)
- return MarkDecorator(Mark(name, (), {}))
+ 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 70177f950..74cf00ee2 100644
--- a/src/pytest/__init__.py
+++ b/src/pytest/__init__.py
@@ -21,7 +21,10 @@ 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 MarkDecorator
+from _pytest.mark import MarkGenerator
from _pytest.mark import param
from _pytest.monkeypatch import MonkeyPatch
from _pytest.nodes import Collector
@@ -89,6 +92,9 @@ __all__ = [
"LogCaptureFixture",
"main",
"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