Skip to content

Commit 813ce45

Browse files
authored
Merge pull request #8179 from bluetech/typing-public-mark
mark: expose Mark, MarkDecorator, MarkGenerator under pytest for typing purposes
2 parents d64706c + 6aa4d1c commit 813ce45

File tree

7 files changed

+89
-38
lines changed

7 files changed

+89
-38
lines changed

changelog/7469.deprecation.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Directly constructing the following classes is now deprecated:
2+
3+
- ``_pytest.mark.structures.Mark``
4+
- ``_pytest.mark.structures.MarkDecorator``
5+
- ``_pytest.mark.structures.MarkGenerator``
6+
7+
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.

changelog/7469.feature.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
The types of objects used in pytest's API are now exported so they may be used in type annotations.
2+
3+
The newly-exported types are:
4+
5+
- ``pytest.Mark`` for :class:`marks <pytest.Mark>`.
6+
- ``pytest.MarkDecorator`` for :class:`mark decorators <pytest.MarkDecorator>`.
7+
- ``pytest.MarkGenerator`` for the :class:`pytest.mark <pytest.MarkGenerator>` singleton.
8+
9+
Constructing them directly is not supported; they are only meant for use in type annotations.
10+
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
11+
12+
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.

doc/en/reference.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ For example:
239239
def test_function():
240240
...
241241
242-
Will create and attach a :class:`Mark <_pytest.mark.structures.Mark>` object to the collected
242+
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
243243
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
244244
:meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>`. The ``mark`` object will have the following attributes:
245245

@@ -849,21 +849,21 @@ Item
849849
MarkDecorator
850850
~~~~~~~~~~~~~
851851

852-
.. autoclass:: _pytest.mark.MarkDecorator
852+
.. autoclass:: pytest.MarkDecorator()
853853
:members:
854854

855855

856856
MarkGenerator
857857
~~~~~~~~~~~~~
858858

859-
.. autoclass:: _pytest.mark.MarkGenerator
859+
.. autoclass:: pytest.MarkGenerator()
860860
:members:
861861

862862

863863
Mark
864864
~~~~
865865

866-
.. autoclass:: _pytest.mark.structures.Mark
866+
.. autoclass:: pytest.Mark()
867867
:members:
868868

869869

src/_pytest/fixtures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
551551
on all function invocations.
552552
553553
:param marker:
554-
A :py:class:`_pytest.mark.MarkDecorator` object created by a call
554+
A :class:`pytest.MarkDecorator` object created by a call
555555
to ``pytest.mark.NAME(...)``.
556556
"""
557557
self.node.add_marker(marker)

src/_pytest/mark/structures.py

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ..compat import NOTSET
2929
from ..compat import NotSetType
3030
from _pytest.config import Config
31+
from _pytest.deprecated import check_ispytest
3132
from _pytest.outcomes import fail
3233
from _pytest.warning_types import PytestUnknownMarkWarning
3334

@@ -200,21 +201,38 @@ def _for_parametrize(
200201

201202

202203
@final
203-
@attr.s(frozen=True)
204+
@attr.s(frozen=True, init=False, auto_attribs=True)
204205
class Mark:
205206
#: Name of the mark.
206-
name = attr.ib(type=str)
207+
name: str
207208
#: Positional arguments of the mark decorator.
208-
args = attr.ib(type=Tuple[Any, ...])
209+
args: Tuple[Any, ...]
209210
#: Keyword arguments of the mark decorator.
210-
kwargs = attr.ib(type=Mapping[str, Any])
211+
kwargs: Mapping[str, Any]
211212

212213
#: Source Mark for ids with parametrize Marks.
213-
_param_ids_from = attr.ib(type=Optional["Mark"], default=None, repr=False)
214+
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
214215
#: Resolved/generated ids with parametrize Marks.
215-
_param_ids_generated = attr.ib(
216-
type=Optional[Sequence[str]], default=None, repr=False
217-
)
216+
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
217+
218+
def __init__(
219+
self,
220+
name: str,
221+
args: Tuple[Any, ...],
222+
kwargs: Mapping[str, Any],
223+
param_ids_from: Optional["Mark"] = None,
224+
param_ids_generated: Optional[Sequence[str]] = None,
225+
*,
226+
_ispytest: bool = False,
227+
) -> None:
228+
""":meta private:"""
229+
check_ispytest(_ispytest)
230+
# Weirdness to bypass frozen=True.
231+
object.__setattr__(self, "name", name)
232+
object.__setattr__(self, "args", args)
233+
object.__setattr__(self, "kwargs", kwargs)
234+
object.__setattr__(self, "_param_ids_from", param_ids_from)
235+
object.__setattr__(self, "_param_ids_generated", param_ids_generated)
218236

219237
def _has_param_ids(self) -> bool:
220238
return "ids" in self.kwargs or len(self.args) >= 4
@@ -243,20 +261,21 @@ def combined_with(self, other: "Mark") -> "Mark":
243261
self.args + other.args,
244262
dict(self.kwargs, **other.kwargs),
245263
param_ids_from=param_ids_from,
264+
_ispytest=True,
246265
)
247266

248267

249268
# A generic parameter designating an object to which a Mark may
250269
# be applied -- a test function (callable) or class.
251270
# Note: a lambda is not allowed, but this can't be represented.
252-
_Markable = TypeVar("_Markable", bound=Union[Callable[..., object], type])
271+
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
253272

254273

255-
@attr.s
274+
@attr.s(init=False, auto_attribs=True)
256275
class MarkDecorator:
257276
"""A decorator for applying a mark on test functions and classes.
258277
259-
MarkDecorators are created with ``pytest.mark``::
278+
``MarkDecorators`` are created with ``pytest.mark``::
260279
261280
mark1 = pytest.mark.NAME # Simple MarkDecorator
262281
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
@@ -267,7 +286,7 @@ class MarkDecorator:
267286
def test_function():
268287
pass
269288
270-
When a MarkDecorator is called it does the following:
289+
When a ``MarkDecorator`` is called, it does the following:
271290
272291
1. If called with a single class as its only positional argument and no
273292
additional keyword arguments, it attaches the mark to the class so it
@@ -276,19 +295,24 @@ def test_function():
276295
2. If called with a single function as its only positional argument and
277296
no additional keyword arguments, it attaches the mark to the function,
278297
containing all the arguments already stored internally in the
279-
MarkDecorator.
298+
``MarkDecorator``.
280299
281-
3. When called in any other case, it returns a new MarkDecorator instance
282-
with the original MarkDecorator's content updated with the arguments
283-
passed to this call.
300+
3. When called in any other case, it returns a new ``MarkDecorator``
301+
instance with the original ``MarkDecorator``'s content updated with
302+
the arguments passed to this call.
284303
285-
Note: The rules above prevent MarkDecorators from storing only a single
286-
function or class reference as their positional argument with no
304+
Note: The rules above prevent a ``MarkDecorator`` from storing only a
305+
single function or class reference as its positional argument with no
287306
additional keyword or positional arguments. You can work around this by
288307
using `with_args()`.
289308
"""
290309

291-
mark = attr.ib(type=Mark, validator=attr.validators.instance_of(Mark))
310+
mark: Mark
311+
312+
def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
313+
""":meta private:"""
314+
check_ispytest(_ispytest)
315+
self.mark = mark
292316

293317
@property
294318
def name(self) -> str:
@@ -307,6 +331,7 @@ def kwargs(self) -> Mapping[str, Any]:
307331

308332
@property
309333
def markname(self) -> str:
334+
""":meta private:"""
310335
return self.name # for backward-compat (2.4.1 had this attr)
311336

312337
def __repr__(self) -> str:
@@ -317,17 +342,15 @@ def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator":
317342
318343
Unlike calling the MarkDecorator, with_args() can be used even
319344
if the sole argument is a callable/class.
320-
321-
:rtype: MarkDecorator
322345
"""
323-
mark = Mark(self.name, args, kwargs)
324-
return self.__class__(self.mark.combined_with(mark))
346+
mark = Mark(self.name, args, kwargs, _ispytest=True)
347+
return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
325348

326349
# Type ignored because the overloads overlap with an incompatible
327350
# return type. Not much we can do about that. Thankfully mypy picks
328351
# the first match so it works out even if we break the rules.
329352
@overload
330-
def __call__(self, arg: _Markable) -> _Markable: # type: ignore[misc]
353+
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
331354
pass
332355

333356
@overload
@@ -386,7 +409,7 @@ def store_mark(obj, mark: Mark) -> None:
386409

387410
class _SkipMarkDecorator(MarkDecorator):
388411
@overload # type: ignore[override,misc]
389-
def __call__(self, arg: _Markable) -> _Markable:
412+
def __call__(self, arg: Markable) -> Markable:
390413
...
391414

392415
@overload
@@ -404,7 +427,7 @@ def __call__( # type: ignore[override]
404427

405428
class _XfailMarkDecorator(MarkDecorator):
406429
@overload # type: ignore[override,misc]
407-
def __call__(self, arg: _Markable) -> _Markable:
430+
def __call__(self, arg: Markable) -> Markable:
408431
...
409432

410433
@overload
@@ -465,9 +488,6 @@ def test_function():
465488
applies a 'slowtest' :class:`Mark` on ``test_function``.
466489
"""
467490

468-
_config: Optional[Config] = None
469-
_markers: Set[str] = set()
470-
471491
# See TYPE_CHECKING above.
472492
if TYPE_CHECKING:
473493
skip: _SkipMarkDecorator
@@ -477,7 +497,13 @@ def test_function():
477497
usefixtures: _UsefixturesMarkDecorator
478498
filterwarnings: _FilterwarningsMarkDecorator
479499

500+
def __init__(self, *, _ispytest: bool = False) -> None:
501+
check_ispytest(_ispytest)
502+
self._config: Optional[Config] = None
503+
self._markers: Set[str] = set()
504+
480505
def __getattr__(self, name: str) -> MarkDecorator:
506+
"""Generate a new :class:`MarkDecorator` with the given name."""
481507
if name[0] == "_":
482508
raise AttributeError("Marker name must NOT start with underscore")
483509

@@ -515,10 +541,10 @@ def __getattr__(self, name: str) -> MarkDecorator:
515541
2,
516542
)
517543

518-
return MarkDecorator(Mark(name, (), {}))
544+
return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
519545

520546

521-
MARK_GEN = MarkGenerator()
547+
MARK_GEN = MarkGenerator(_ispytest=True)
522548

523549

524550
@final

src/pytest/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
from _pytest.freeze_support import freeze_includes
2222
from _pytest.logging import LogCaptureFixture
2323
from _pytest.main import Session
24+
from _pytest.mark import Mark
2425
from _pytest.mark import MARK_GEN as mark
26+
from _pytest.mark import MarkDecorator
27+
from _pytest.mark import MarkGenerator
2528
from _pytest.mark import param
2629
from _pytest.monkeypatch import MonkeyPatch
2730
from _pytest.nodes import Collector
@@ -89,6 +92,9 @@
8992
"LogCaptureFixture",
9093
"main",
9194
"mark",
95+
"Mark",
96+
"MarkDecorator",
97+
"MarkGenerator",
9298
"Module",
9399
"MonkeyPatch",
94100
"Package",

testing/test_mark.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_pytest_exists_in_namespace_all(self, attr: str, modulename: str) -> Non
2121
assert attr in module.__all__ # type: ignore
2222

2323
def test_pytest_mark_notcallable(self) -> None:
24-
mark = MarkGenerator()
24+
mark = MarkGenerator(_ispytest=True)
2525
with pytest.raises(TypeError):
2626
mark() # type: ignore[operator]
2727

@@ -40,7 +40,7 @@ class SomeClass:
4040
assert pytest.mark.foo.with_args(SomeClass) is not SomeClass # type: ignore[comparison-overlap]
4141

4242
def test_pytest_mark_name_starts_with_underscore(self) -> None:
43-
mark = MarkGenerator()
43+
mark = MarkGenerator(_ispytest=True)
4444
with pytest.raises(AttributeError):
4545
mark._some_name
4646

0 commit comments

Comments
 (0)