-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
use callable protocols for pytest.skip/exit/fail/xfail instead of _WithException wrapper with __call__ attribute #13445
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
Changes from 2 commits
da59835
bc8cb62
52a1d39
bbffb2f
4183b10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Made the type annotations of :func:`pytest.skip` and friends more spec-complaint to have them work across more type checkers. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,9 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable | ||
| import sys | ||
| from typing import Any | ||
| from typing import cast | ||
| from typing import NoReturn | ||
| from typing import Protocol | ||
| from typing import TypeVar | ||
|
|
||
| from .warning_types import PytestDeprecationWarning | ||
|
|
||
|
|
@@ -77,132 +73,118 @@ def __init__( | |
| super().__init__(msg) | ||
|
|
||
|
|
||
| # We need a callable protocol to add attributes, for discussion see | ||
| # https://github.com/python/mypy/issues/2087. | ||
| class XFailed(Failed): | ||
| """Raised from an explicit call to pytest.xfail().""" | ||
|
|
||
| _F = TypeVar("_F", bound=Callable[..., object]) | ||
| _ET = TypeVar("_ET", bound=type[BaseException]) | ||
|
|
||
| class _Exit: | ||
| Exception: type[Exit] = Exit | ||
|
|
||
| class _WithException(Protocol[_F, _ET]): | ||
| Exception: _ET | ||
| __call__: _F | ||
| def __call__(self, reason: str = "", returncode: int | None = None) -> NoReturn: | ||
| """Exit testing process. | ||
|
Member
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. Let's put the docstring on the class, so that sphinx picks it up correctly. You can verify that it works by checking that it shows up in the PR docs preview (currently empty): https://pytest--13445.org.readthedocs.build/en/13445/reference/reference.html#pytest-skip
Contributor
Author
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. Oh this docs pipeline is neat, didn't notice that! Moved and seems like it's picked up correctly now |
||
|
|
||
| :param reason: | ||
| The message to show as the reason for exiting pytest. reason has a default value | ||
| only because `msg` is deprecated. | ||
|
|
||
| def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _ET]]: | ||
| def decorate(func: _F) -> _WithException[_F, _ET]: | ||
| func_with_exception = cast(_WithException[_F, _ET], func) | ||
| func_with_exception.Exception = exception_type | ||
| return func_with_exception | ||
| :param returncode: | ||
| Return code to be used when exiting pytest. None means the same as ``0`` (no error), | ||
| same as :func:`sys.exit`. | ||
|
|
||
| return decorate | ||
| :raises pytest.exit.Exception: | ||
| The exception that is raised. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise Exit(msg=reason, returncode=returncode) | ||
|
|
||
|
|
||
| # Exposed helper methods. | ||
| class _Skip: | ||
| Exception: type[Skipped] = Skipped | ||
|
|
||
| def __call__(self, reason: str = "", allow_module_level: bool = False) -> NoReturn: | ||
| """Skip an executing test with the given message. | ||
|
|
||
| @_with_exception(Exit) | ||
| def exit( | ||
| reason: str = "", | ||
| returncode: int | None = None, | ||
| ) -> NoReturn: | ||
| """Exit testing process. | ||
| This function should be called only during testing (setup, call or teardown) or | ||
| during collection by using the ``allow_module_level`` flag. This function can | ||
| be called in doctests as well. | ||
|
|
||
| :param reason: | ||
| The message to show as the reason for exiting pytest. reason has a default value | ||
| only because `msg` is deprecated. | ||
| :param reason: | ||
| The message to show the user as reason for the skip. | ||
|
|
||
| :param returncode: | ||
| Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`. | ||
| :param allow_module_level: | ||
| Allows this function to be called at module level. | ||
| Raising the skip exception at module level will stop | ||
| the execution of the module and prevent the collection of all tests in the module, | ||
| even those defined before the `skip` call. | ||
|
|
||
| :raises pytest.exit.Exception: | ||
| The exception that is raised. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise Exit(reason, returncode) | ||
| Defaults to False. | ||
|
|
||
| :raises pytest.skip.Exception: | ||
| The exception that is raised. | ||
|
|
||
| @_with_exception(Skipped) | ||
| def skip( | ||
| reason: str = "", | ||
| *, | ||
| allow_module_level: bool = False, | ||
| ) -> NoReturn: | ||
| """Skip an executing test with the given message. | ||
|
|
||
| This function should be called only during testing (setup, call or teardown) or | ||
| during collection by using the ``allow_module_level`` flag. This function can | ||
| be called in doctests as well. | ||
| .. note:: | ||
| It is better to use the :ref:`pytest.mark.skipif ref` marker when | ||
| possible to declare a test to be skipped under certain conditions | ||
| like mismatching platforms or dependencies. | ||
| Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`) | ||
| to skip a doctest statically. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise Skipped(msg=reason, allow_module_level=allow_module_level) | ||
|
|
||
| :param reason: | ||
| The message to show the user as reason for the skip. | ||
|
|
||
| :param allow_module_level: | ||
| Allows this function to be called at module level. | ||
| Raising the skip exception at module level will stop | ||
| the execution of the module and prevent the collection of all tests in the module, | ||
| even those defined before the `skip` call. | ||
| class _Fail: | ||
| Exception: type[Failed] = Failed | ||
|
|
||
| Defaults to False. | ||
|
|
||
| :raises pytest.skip.Exception: | ||
| The exception that is raised. | ||
|
|
||
| .. note:: | ||
| It is better to use the :ref:`pytest.mark.skipif ref` marker when | ||
| possible to declare a test to be skipped under certain conditions | ||
| like mismatching platforms or dependencies. | ||
| Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`) | ||
| to skip a doctest statically. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise Skipped(msg=reason, allow_module_level=allow_module_level) | ||
| def __call__(self, reason: str = "", pytrace: bool = True) -> NoReturn: | ||
| """Explicitly fail an executing test with the given message. | ||
|
|
||
| :param reason: | ||
| The message to show the user as reason for the failure. | ||
|
|
||
| @_with_exception(Failed) | ||
| def fail(reason: str = "", pytrace: bool = True) -> NoReturn: | ||
| """Explicitly fail an executing test with the given message. | ||
| :param pytrace: | ||
| If False, msg represents the full failure information and no | ||
| python traceback will be reported. | ||
|
|
||
| :param reason: | ||
| The message to show the user as reason for the failure. | ||
| :raises pytest.fail.Exception: | ||
| The exception that is raised. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise Failed(msg=reason, pytrace=pytrace) | ||
|
|
||
| :param pytrace: | ||
| If False, msg represents the full failure information and no | ||
| python traceback will be reported. | ||
|
|
||
| :raises pytest.fail.Exception: | ||
| The exception that is raised. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise Failed(msg=reason, pytrace=pytrace) | ||
| class _XFail: | ||
| Exception: type[XFailed] = XFailed | ||
|
|
||
| def __call__(self, reason: str = "") -> NoReturn: | ||
| """Imperatively xfail an executing test or setup function with the given reason. | ||
|
|
||
| class XFailed(Failed): | ||
| """Raised from an explicit call to pytest.xfail().""" | ||
| This function should be called only during testing (setup, call or teardown). | ||
|
|
||
| No other code is executed after using ``xfail()`` (it is implemented | ||
| internally by raising an exception). | ||
|
|
||
| @_with_exception(XFailed) | ||
| def xfail(reason: str = "") -> NoReturn: | ||
| """Imperatively xfail an executing test or setup function with the given reason. | ||
| :param reason: | ||
| The message to show the user as reason for the xfail. | ||
|
|
||
| This function should be called only during testing (setup, call or teardown). | ||
| .. note:: | ||
| It is better to use the :ref:`pytest.mark.xfail ref` marker when | ||
| possible to declare a test to be xfailed under certain conditions | ||
| like known bugs or missing features. | ||
|
|
||
| No other code is executed after using ``xfail()`` (it is implemented | ||
| internally by raising an exception). | ||
| :raises pytest.xfail.Exception: | ||
| The exception that is raised. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise XFailed(msg=reason) | ||
|
|
||
| :param reason: | ||
| The message to show the user as reason for the xfail. | ||
|
|
||
| .. note:: | ||
| It is better to use the :ref:`pytest.mark.xfail ref` marker when | ||
| possible to declare a test to be xfailed under certain conditions | ||
| like known bugs or missing features. | ||
| # Exposed helper methods. | ||
|
Member
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. My slight preference it to have the
Contributor
Author
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. done! |
||
|
|
||
| :raises pytest.xfail.Exception: | ||
| The exception that is raised. | ||
| """ | ||
| __tracebackhide__ = True | ||
| raise XFailed(reason) | ||
| exit: _Exit = _Exit() | ||
|
Member
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. (Here too I think we can omit the explicit type annotations)
Contributor
Author
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. So I added them because otherwise So if we remove explicit annotation, ty would infer I can remove the Let me know what you think!
Member
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. Can't say I understand the rationale of ty to behave this way, but it's intentional and it's just a minor style thing. So let's keep the annotations. |
||
| skip: _Skip = _Skip() | ||
| fail: _Fail = _Fail() | ||
| xfail: _XFail = _XFail() | ||
|
|
||
|
|
||
| def importorskip( | ||
|
|
||
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.
Usually we omit an explicit annotation when it is inferred.
Also, here I think a
ClassVarannotation would be appropriate. SoException: ClassVar = Exit.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.
Good idea, added
ClassVar-- see below comment re: type annotation