-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
mark: expose Mark, MarkDecorator, MarkGenerator under pytest for typing purposes #8179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ | |
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 @@ def _for_parametrize( | |
|
||
|
||
@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 @@ def combined_with(self, other: "Mark") -> "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 @@ def test_function(): | |
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 | ||
nicoddemus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 @@ def kwargs(self) -> Mapping[str, Any]: | |
|
||
@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 @@ def with_args(self, *args: object, **kwargs: object) -> "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 @@ def store_mark(obj, mark: Mark) -> None: | |
|
||
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 @@ def __call__( # type: ignore[override] | |
|
||
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 @@ def test_function(): | |
applies a 'slowtest' :class:`Mark` on ``test_function``. | ||
""" | ||
|
||
_config: Optional[Config] = None | ||
_markers: Set[str] = set() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ouch! |
||
|
||
# See TYPE_CHECKING above. | ||
if TYPE_CHECKING: | ||
skip: _SkipMarkDecorator | ||
|
@@ -477,7 +497,13 @@ def test_function(): | |
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 @@ def __getattr__(self, name: str) -> MarkDecorator: | |
2, | ||
) | ||
|
||
return MarkDecorator(Mark(name, (), {})) | ||
return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True) | ||
|
||
|
||
MARK_GEN = MarkGenerator() | ||
MARK_GEN = MarkGenerator(_ispytest=True) | ||
|
||
|
||
@final | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we introduce a metaclass that mangles the
_ispytest
in__call__
- making all those classes have strange ctors is a painThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO I prefer the
_is_pytest
approach because it is more explicit... I think a metaclass introduces needless complexity just for this change.Just stating my opinion though, not against changing it to a metaclass if Ran also prefers to do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel simillary to @nicoddemus, I think that while the
_ispytest
is a bit annoying, it is better in most other dimensions (like ease of understanding, greppability, ease of type checking, ease of bypassing when needed), so I prefer it.