From 264e7ac32733572fad3892ba0e830dba7cc87cf0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 14 Jun 2023 09:22:34 +0300 Subject: [PATCH 01/53] reference: add doc for `Package` We document the other py collectors, we should document `Package` as well. --- doc/en/reference/reference.rst | 6 ++++++ src/_pytest/python.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 7107218b3..f591eb21e 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -926,6 +926,12 @@ Parser .. autoclass:: pytest.Parser() :members: +Package +~~~~~~~ + +.. autoclass:: pytest.Package() + :members: + OptionGroup ~~~~~~~~~~~ diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ad847c8af..2036819cf 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -659,6 +659,9 @@ class Module(nodes.File, PyCollector): class Package(Module): + """Collector for files and directories in a Python packages -- directories + with an `__init__.py` file.""" + def __init__( self, fspath: Optional[LEGACY_PATH], From bd88a6412d7518c94ebdb4bbfcff39accd771d4f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 14 Jun 2023 09:19:56 +0300 Subject: [PATCH 02/53] reference: separate node types to their own section I think it's helpful to separate the node classes from the other objects, as they have their own unique usage. I've chosen not to alphabetize the order, but to use a logical order instead. Also slightly improve the docstrings. --- doc/en/reference/reference.rst | 150 +++++++++++++++++---------------- src/_pytest/main.py | 5 ++ src/_pytest/nodes.py | 21 +++-- src/_pytest/python.py | 12 ++- 4 files changed, 102 insertions(+), 86 deletions(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index f591eb21e..6154bbf25 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -783,18 +783,66 @@ reporting or interaction with exceptions: .. autofunction:: pytest_leave_pdb -Objects -------- +Collection tree objects +----------------------- -Full reference to objects accessible from :ref:`fixtures ` or :ref:`hooks `. +These are the collector and item classes (collectively called "nodes") which +make up the collection tree. +Node +~~~~ -CallInfo -~~~~~~~~ - -.. autoclass:: pytest.CallInfo() +.. autoclass:: _pytest.nodes.Node() :members: +Collector +~~~~~~~~~ + +.. autoclass:: pytest.Collector() + :members: + :show-inheritance: + +Item +~~~~ + +.. autoclass:: pytest.Item() + :members: + :show-inheritance: + +File +~~~~ + +.. autoclass:: pytest.File() + :members: + :show-inheritance: + +FSCollector +~~~~~~~~~~~ + +.. autoclass:: _pytest.nodes.FSCollector() + :members: + :show-inheritance: + +Session +~~~~~~~ + +.. autoclass:: pytest.Session() + :members: + :show-inheritance: + +Package +~~~~~~~ + +.. autoclass:: pytest.Package() + :members: + :show-inheritance: + +Module +~~~~~~ + +.. autoclass:: pytest.Module() + :members: + :show-inheritance: Class ~~~~~ @@ -803,13 +851,34 @@ Class :members: :show-inheritance: -Collector -~~~~~~~~~ +Function +~~~~~~~~ -.. autoclass:: pytest.Collector() +.. autoclass:: pytest.Function() :members: :show-inheritance: +FunctionDefinition +~~~~~~~~~~~~~~~~~~ + +.. autoclass:: _pytest.python.FunctionDefinition() + :members: + :show-inheritance: + + +Objects +------- + +Objects accessible from :ref:`fixtures ` or :ref:`hooks ` +or importable from ``pytest``. + + +CallInfo +~~~~~~~~ + +.. autoclass:: pytest.CallInfo() + :members: + CollectReport ~~~~~~~~~~~~~ @@ -837,13 +906,6 @@ ExitCode .. autoclass:: pytest.ExitCode :members: -File -~~~~ - -.. autoclass:: pytest.File() - :members: - :show-inheritance: - FixtureDef ~~~~~~~~~~ @@ -852,34 +914,6 @@ FixtureDef :members: :show-inheritance: -FSCollector -~~~~~~~~~~~ - -.. autoclass:: _pytest.nodes.FSCollector() - :members: - :show-inheritance: - -Function -~~~~~~~~ - -.. autoclass:: pytest.Function() - :members: - :show-inheritance: - -FunctionDefinition -~~~~~~~~~~~~~~~~~~ - -.. autoclass:: _pytest.python.FunctionDefinition() - :members: - :show-inheritance: - -Item -~~~~ - -.. autoclass:: pytest.Item() - :members: - :show-inheritance: - MarkDecorator ~~~~~~~~~~~~~ @@ -907,31 +941,12 @@ Metafunc .. autoclass:: pytest.Metafunc() :members: -Module -~~~~~~ - -.. autoclass:: pytest.Module() - :members: - :show-inheritance: - -Node -~~~~ - -.. autoclass:: _pytest.nodes.Node() - :members: - Parser ~~~~~~ .. autoclass:: pytest.Parser() :members: -Package -~~~~~~~ - -.. autoclass:: pytest.Package() - :members: - OptionGroup ~~~~~~~~~~~ @@ -947,13 +962,6 @@ PytestPluginManager :inherited-members: :show-inheritance: -Session -~~~~~~~ - -.. autoclass:: pytest.Session() - :members: - :show-inheritance: - TestReport ~~~~~~~~~~ diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 155d4300e..803b95a20 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -462,6 +462,11 @@ class _bestrelpath_cache(Dict[Path, str]): @final class Session(nodes.FSCollector): + """The root of the collection tree. + + ``Session`` collects the initial paths given as arguments to pytest. + """ + Interrupted = Interrupted Failed = Failed # Set on the session by runner.pytest_sessionstart. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index dbd6b0a42..667a02b77 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -157,10 +157,11 @@ class NodeMeta(type): class Node(metaclass=NodeMeta): - """Base class for Collector and Item, the components of the test - collection tree. + r"""Base class of :class:`Collector` and :class:`Item`, the components of + the test collection tree. - Collector subclasses have children; Items are leaf nodes. + ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the + leaf nodes. """ # Implemented in the legacypath plugin. @@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i class Collector(Node): - """Collector instances create children through collect() and thus - iteratively build a tree.""" + """Base class of all collectors. + + Collector create children through `collect()` and thus iteratively build + the collection tree. + """ class CollectError(Exception): """An error during collection, contains a custom message.""" def collect(self) -> Iterable[Union["Item", "Collector"]]: - """Return a list of children (items and collectors) for this - collection node.""" + """Collect children (items and collectors) for this collector.""" raise NotImplementedError("abstract") # TODO: This omits the style= parameter which breaks Liskov Substitution. @@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[ class FSCollector(Collector): + """Base class for filesystem collectors.""" + def __init__( self, fspath: Optional[LEGACY_PATH] = None, @@ -660,7 +665,7 @@ class File(FSCollector): class Item(Node): - """A basic test invocation item. + """Base class of all test invocation items. Note that for a single function there might be multiple test invocation items. """ diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2036819cf..b24dc90d8 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -522,7 +522,7 @@ class PyCollector(PyobjMixin, nodes.Collector): class Module(nodes.File, PyCollector): - """Collector for test classes and functions.""" + """Collector for test classes and functions in a Python module.""" def _getobj(self): return self._importtestmodule() @@ -791,7 +791,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o class Class(PyCollector): - """Collector for test methods.""" + """Collector for test methods (and nested classes) in a Python class.""" @classmethod def from_parent(cls, parent, *, name, obj=None, **kw): @@ -1676,7 +1676,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: class Function(PyobjMixin, nodes.Item): - """An Item responsible for setting up and executing a Python test function. + """Item responsible for setting up and executing a Python test function. :param name: The full function name, including any decorations like those @@ -1833,10 +1833,8 @@ class Function(PyobjMixin, nodes.Item): class FunctionDefinition(Function): - """ - This class is a step gap solution until we evolve to have actual function definition nodes - and manage to get rid of ``metafunc``. - """ + """This class is a stop gap solution until we evolve to have actual function + definition nodes and manage to get rid of ``metafunc``.""" def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be run as tests") From 9859c110cc9851a25802524f09fc58ce2aa018db Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani <86737547+tushar-deepsource@users.noreply.github.com> Date: Mon, 26 Jun 2023 12:14:41 +0530 Subject: [PATCH 03/53] Ensure the docstring is a string --- src/_pytest/assertion/rewrite.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ab83fee32..9ac4d582f 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -687,6 +687,7 @@ class AssertionRewriter(ast.NodeVisitor): expect_docstring and isinstance(item, ast.Expr) and isinstance(item.value, astStr) + and isinstance(item.value.value, str) ): if sys.version_info >= (3, 8): doc = item.value.value From 4ebb2b94c289a5d1a5756a4a69c7f2ec3e0def89 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani <86737547+tushar-deepsource@users.noreply.github.com> Date: Mon, 26 Jun 2023 12:19:42 +0530 Subject: [PATCH 04/53] fix code for py37 --- src/_pytest/assertion/rewrite.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 9ac4d582f..5d3dfcc31 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -687,12 +687,13 @@ class AssertionRewriter(ast.NodeVisitor): expect_docstring and isinstance(item, ast.Expr) and isinstance(item.value, astStr) - and isinstance(item.value.value, str) ): if sys.version_info >= (3, 8): doc = item.value.value else: doc = item.value.s + if not isinstance(doc, str): + return if self.is_rewrite_disabled(doc): return expect_docstring = False From 05d7e609040f0af87b2d647ef10b9deb7dd89d3c Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Mon, 26 Jun 2023 12:37:23 +0530 Subject: [PATCH 05/53] Add test --- testing/test_assertrewrite.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 778f843e6..9b4ca87b2 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -2055,3 +2055,14 @@ class TestReprSizeVerbosity: self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10) result = pytester.runpytest("-vv") result.stdout.no_fnmatch_line("*xxx...xxx*") + + +class TestIssue11140: + def test_constant_not_picked_as_module_docstring(self, pytester: Pytester) -> None: + pytester.makepyfile( + """\ + 0 + """ + ) + result = pytester.runpytest() + assert result.ret == 0 From a14fc10cac90dcb075221d03c52c76dd714daab1 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani Date: Mon, 26 Jun 2023 13:06:11 +0530 Subject: [PATCH 06/53] add a test function to the test --- testing/test_assertrewrite.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 9b4ca87b2..5833a0483 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -2062,6 +2062,9 @@ class TestIssue11140: pytester.makepyfile( """\ 0 + + def test_foo(): + pass """ ) result = pytester.runpytest() From 5332656906dc60b29b4288eaf1842452106fce46 Mon Sep 17 00:00:00 2001 From: Tushar Sadhwani <86737547+tushar-deepsource@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:02:33 +0530 Subject: [PATCH 07/53] Add changelog and author --- AUTHORS | 1 + changelog/11146.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/11146.bugfix.rst diff --git a/AUTHORS b/AUTHORS index ee4ef203f..2acbdb98c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -370,6 +370,7 @@ Tomer Keren Tony Narlock Tor Colvin Trevor Bekolay +Tushar Sadhwani Tyler Goodlet Tzu-ping Chung Vasily Kuznetsov diff --git a/changelog/11146.bugfix.rst b/changelog/11146.bugfix.rst new file mode 100644 index 000000000..b1794ad57 --- /dev/null +++ b/changelog/11146.bugfix.rst @@ -0,0 +1 @@ +- Prevent constants at the top of file from being detected as docstrings. \ No newline at end of file From 4719d998d809a99937fc571c7afba6f8291a97d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:34:02 +0000 Subject: [PATCH 08/53] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/11146.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/11146.bugfix.rst b/changelog/11146.bugfix.rst index b1794ad57..03b468f30 100644 --- a/changelog/11146.bugfix.rst +++ b/changelog/11146.bugfix.rst @@ -1 +1 @@ -- Prevent constants at the top of file from being detected as docstrings. \ No newline at end of file +- Prevent constants at the top of file from being detected as docstrings. From f4e3b4ad9827276ff20c2c3a86e0637a37b09019 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 14:55:42 -0700 Subject: [PATCH 09/53] Drop Python 3.7 --- changelog/11151.breaking.rst | 2 + doc/en/conf.py | 21 --------- setup.cfg | 4 +- src/_pytest/_code/code.py | 2 +- src/_pytest/_code/source.py | 3 +- src/_pytest/_io/terminalwriter.py | 2 +- src/_pytest/assertion/rewrite.py | 66 ++++++++++++----------------- src/_pytest/cacheprovider.py | 2 +- src/_pytest/capture.py | 2 +- src/_pytest/compat.py | 63 --------------------------- src/_pytest/config/__init__.py | 9 ++-- src/_pytest/config/argparsing.py | 2 +- src/_pytest/config/exceptions.py | 2 +- src/_pytest/fixtures.py | 11 ++--- src/_pytest/legacypath.py | 2 +- src/_pytest/logging.py | 2 +- src/_pytest/main.py | 4 +- src/_pytest/mark/expression.py | 9 +--- src/_pytest/mark/structures.py | 2 +- src/_pytest/monkeypatch.py | 2 +- src/_pytest/nodes.py | 2 +- src/_pytest/outcomes.py | 13 +----- src/_pytest/pytester.py | 2 +- src/_pytest/python.py | 2 +- src/_pytest/python_api.py | 13 +++--- src/_pytest/recwarn.py | 4 +- src/_pytest/reports.py | 2 +- src/_pytest/runner.py | 2 +- src/_pytest/terminal.py | 2 +- src/_pytest/tmpdir.py | 24 ++++------- src/_pytest/warning_types.py | 3 +- testing/acceptance_test.py | 4 +- testing/code/test_source.py | 9 +--- testing/python/metafunc.py | 22 ---------- testing/test_assertion.py | 4 +- testing/test_assertrewrite.py | 17 +------- testing/test_compat.py | 2 +- testing/test_config.py | 14 +++--- testing/test_entry_points.py | 4 +- testing/test_skipping.py | 6 +-- testing/test_threadexception.py | 6 --- testing/test_unittest.py | 9 ---- testing/test_unraisableexception.py | 6 --- 43 files changed, 99 insertions(+), 285 deletions(-) create mode 100644 changelog/11151.breaking.rst diff --git a/changelog/11151.breaking.rst b/changelog/11151.breaking.rst new file mode 100644 index 000000000..d846fc94c --- /dev/null +++ b/changelog/11151.breaking.rst @@ -0,0 +1,2 @@ +Drop support for Python 3.7, which `reached end-of-life on 2023-06-27 +`__. diff --git a/doc/en/conf.py b/doc/en/conf.py index 32f508219..92607a15a 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -15,12 +15,10 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -import ast import os import shutil import sys from textwrap import dedent -from typing import List from typing import TYPE_CHECKING from _pytest import __version__ as version @@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None: configure_logging(app) - # Make Sphinx mark classes with "final" when decorated with @final. - # We need this because we import final from pytest._compat, not from - # typing (for Python < 3.8 compat), so Sphinx doesn't detect it. - # To keep things simple we accept any `@final` decorator. - # Ref: https://github.com/pytest-dev/pytest/pull/7780 - import sphinx.pycode.ast - import sphinx.pycode.parser - - original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final - - def patched_is_final(self, decorators: List[ast.expr]) -> bool: - if original_is_final(self, decorators): - return True - return any( - sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators - ) - - sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final - # legacypath.py monkey-patches pytest.Testdir in. Import the file so # that autodoc can discover references to it. import _pytest.legacypath # noqa: F401 diff --git a/setup.cfg b/setup.cfg index bc93b1c06..f80665f42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,6 @@ classifiers = Operating System :: POSIX Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -50,9 +49,8 @@ install_requires = pluggy>=0.12,<2.0 colorama;sys_platform=="win32" exceptiongroup>=1.0.0rc8;python_version<"3.11" - importlib-metadata>=0.12;python_version<"3.8" tomli>=1.0.0;python_version<"3.11" -python_requires = >=3.7 +python_requires = >=3.8 package_dir = =src setup_requires = diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 9b051332b..ddc03d752 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -17,6 +17,7 @@ from typing import Any from typing import Callable from typing import ClassVar from typing import Dict +from typing import final from typing import Generic from typing import Iterable from typing import List @@ -42,7 +43,6 @@ from _pytest._code.source import Source from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.deprecated import check_ispytest from _pytest.pathlib import absolutepath diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 208cfb800..cc7ac407e 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i values: List[int] = [] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): - # Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. - # Since Python 3.8, the lineno points to the class/def, so need to include the decorators. + # The lineno points to the class/def, so need to include the decorators. if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): for d in x.decorator_list: values.append(d.lineno - 1) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 379035d85..eb1b46939 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -2,12 +2,12 @@ import os import shutil import sys +from typing import final from typing import Optional from typing import Sequence from typing import TextIO from .wcwidth import wcswidth -from _pytest.compat import final # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ab83fee32..157903faf 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -44,17 +44,6 @@ from _pytest.stash import StashKey if TYPE_CHECKING: from _pytest.assertion import AssertionState -if sys.version_info >= (3, 8): - namedExpr = ast.NamedExpr - astNameConstant = ast.Constant - astStr = ast.Constant - astNum = ast.Constant -else: - namedExpr = ast.Expr - astNameConstant = ast.NameConstant - astStr = ast.Str - astNum = ast.Num - assertstate_key = StashKey["AssertionState"]() @@ -686,12 +675,9 @@ class AssertionRewriter(ast.NodeVisitor): if ( expect_docstring and isinstance(item, ast.Expr) - and isinstance(item.value, astStr) + and isinstance(item.value, ast.Constant) ): - if sys.version_info >= (3, 8): - doc = item.value.value - else: - doc = item.value.s + doc = item.value.value if self.is_rewrite_disabled(doc): return expect_docstring = False @@ -823,7 +809,7 @@ class AssertionRewriter(ast.NodeVisitor): current = self.stack.pop() if self.stack: self.explanation_specifiers = self.stack[-1] - keys = [astStr(key) for key in current.keys()] + keys = [ast.Constant(key) for key in current.keys()] format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) @@ -877,16 +863,16 @@ class AssertionRewriter(ast.NodeVisitor): negation = ast.UnaryOp(ast.Not(), top_condition) if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - msg = self.pop_format_context(astStr(explanation)) + msg = self.pop_format_context(ast.Constant(explanation)) # Failed if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) gluestr = "\n>assert " else: - assertmsg = astStr("") + assertmsg = ast.Constant("") gluestr = "assert " - err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg) + err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_name = ast.Name("AssertionError", ast.Load()) fmt = self.helper("_format_explanation", err_msg) @@ -902,8 +888,8 @@ class AssertionRewriter(ast.NodeVisitor): hook_call_pass = ast.Expr( self.helper( "_call_assertion_pass", - astNum(assert_.lineno), - astStr(orig), + ast.Constant(assert_.lineno), + ast.Constant(orig), fmt_pass, ) ) @@ -922,7 +908,7 @@ class AssertionRewriter(ast.NodeVisitor): variables = [ ast.Name(name, ast.Store()) for name in self.format_variables ] - clear_format = ast.Assign(variables, astNameConstant(None)) + clear_format = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear_format) else: # Original assertion rewriting @@ -933,9 +919,9 @@ class AssertionRewriter(ast.NodeVisitor): assertmsg = self.helper("_format_assertmsg", assert_.msg) explanation = "\n>assert " + explanation else: - assertmsg = astStr("") + assertmsg = ast.Constant("") explanation = "assert " + explanation - template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation)) + template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation)) msg = self.pop_format_context(template) fmt = self.helper("_format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) @@ -947,7 +933,7 @@ class AssertionRewriter(ast.NodeVisitor): # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] - clear = ast.Assign(variables, astNameConstant(None)) + clear = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear) # Fix locations (line numbers/column offsets). for stmt in self.statements: @@ -955,26 +941,26 @@ class AssertionRewriter(ast.NodeVisitor): ast.copy_location(node, assert_) return self.statements - def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]: + def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]: # This method handles the 'walrus operator' repr of the target # name if it's a local variable or _should_repr_global_name() # thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) target_id = name.target.id # type: ignore[attr-defined] - inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), astStr(target_id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(target_id)) return name, self.explanation_param(expr) def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: # Display the repr of the name if it's a local variable or # _should_repr_global_name() thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) - inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), astStr(name.id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(name.id)) return name, self.explanation_param(expr) def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: @@ -993,10 +979,10 @@ class AssertionRewriter(ast.NodeVisitor): # cond is set in a prior loop iteration below self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts = fail_inner - # Check if the left operand is a namedExpr and the value has already been visited + # Check if the left operand is a ast.NamedExpr and the value has already been visited if ( isinstance(v, ast.Compare) - and isinstance(v.left, namedExpr) + and isinstance(v.left, ast.NamedExpr) and v.left.target.id in [ ast_expr.id @@ -1012,7 +998,7 @@ class AssertionRewriter(ast.NodeVisitor): self.push_format_context() res, expl = self.visit(v) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) - expl_format = self.pop_format_context(astStr(expl)) + expl_format = self.pop_format_context(ast.Constant(expl)) call = ast.Call(app, [expl_format], []) self.expl_stmts.append(ast.Expr(call)) if i < levels: @@ -1024,7 +1010,7 @@ class AssertionRewriter(ast.NodeVisitor): self.statements = body = inner self.statements = save self.expl_stmts = fail_save - expl_template = self.helper("_format_boolop", expl_list, astNum(is_or)) + expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or)) expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) @@ -1098,7 +1084,7 @@ class AssertionRewriter(ast.NodeVisitor): comp.left = self.variables_overwrite[ comp.left.id ] # type:ignore[assignment] - if isinstance(comp.left, namedExpr): + if isinstance(comp.left, ast.NamedExpr): self.variables_overwrite[ comp.left.target.id ] = comp.left # type:ignore[assignment] @@ -1114,7 +1100,7 @@ class AssertionRewriter(ast.NodeVisitor): results = [left_res] for i, op, next_operand in it: if ( - isinstance(next_operand, namedExpr) + isinstance(next_operand, ast.NamedExpr) and isinstance(left_res, ast.Name) and next_operand.target.id == left_res.id ): @@ -1127,9 +1113,9 @@ class AssertionRewriter(ast.NodeVisitor): next_expl = f"({next_expl})" results.append(next_res) sym = BINOP_MAP[op.__class__] - syms.append(astStr(sym)) + syms.append(ast.Constant(sym)) expl = f"{left_expl} {sym} {next_expl}" - expls.append(astStr(expl)) + expls.append(ast.Constant(expl)) res_expr = ast.Compare(left_res, [op], [next_res]) self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl @@ -1173,7 +1159,7 @@ def try_makedirs(cache_dir: Path) -> bool: def get_cache_dir(file_path: Path) -> Path: """Return the cache directory to write .pyc files for the given .py file path.""" - if sys.version_info >= (3, 8) and sys.pycache_prefix: + if sys.pycache_prefix: # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 855716d81..a0029d6a0 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,6 +6,7 @@ import json import os from pathlib import Path from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import List @@ -18,7 +19,6 @@ from .pathlib import rm_rf from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index a8ca0869f..3e31e3cb7 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -11,6 +11,7 @@ from types import TracebackType from typing import Any from typing import AnyStr from typing import BinaryIO +from typing import final from typing import Generator from typing import Generic from typing import Iterable @@ -24,7 +25,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union -from _pytest.compat import final from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 352211de8..4736bf40d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -12,23 +12,12 @@ from inspect import signature from pathlib import Path from typing import Any from typing import Callable -from typing import Generic from typing import NoReturn from typing import TYPE_CHECKING from typing import TypeVar import py -# fmt: off -# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351. -# If `overload` is imported from `compat` instead of from `typing`, -# Sphinx doesn't recognize it as `overload` and the API docs for -# overloaded functions look good again. But type checkers handle -# it fine. -# fmt: on -if True: - from typing import overload as overload - if TYPE_CHECKING: from typing_extensions import Final @@ -58,17 +47,6 @@ class NotSetType(enum.Enum): NOTSET: Final = NotSetType.token # noqa: E305 # fmt: on -if sys.version_info >= (3, 8): - import importlib.metadata - - importlib_metadata = importlib.metadata -else: - import importlib_metadata as importlib_metadata # noqa: F401 - - -def _format_args(func: Callable[..., Any]) -> str: - return str(signature(func)) - def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) @@ -338,47 +316,6 @@ def safe_isclass(obj: object) -> bool: return False -if TYPE_CHECKING: - if sys.version_info >= (3, 8): - from typing import final as final - else: - from typing_extensions import final as final -elif sys.version_info >= (3, 8): - from typing import final as final -else: - - def final(f): - return f - - -if sys.version_info >= (3, 8): - from functools import cached_property as cached_property -else: - - class cached_property(Generic[_S, _T]): - __slots__ = ("func", "__doc__") - - def __init__(self, func: Callable[[_S], _T]) -> None: - self.func = func - self.__doc__ = func.__doc__ - - @overload - def __get__( - self, instance: None, owner: type[_S] | None = ... - ) -> cached_property[_S, _T]: - ... - - @overload - def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T: - ... - - def __get__(self, instance, owner=None): - if instance is None: - return self - value = instance.__dict__[self.func.__name__] = self.func(instance) - return value - - def get_user_id() -> int | None: """Return the current user id, or None if we cannot get it reliably on the current platform.""" # win32 does not have a getuid() function. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c9a4b7f63..47651ad9f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -5,6 +5,7 @@ import copy import dataclasses import enum import glob +import importlib.metadata import inspect import os import re @@ -21,6 +22,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generator from typing import IO from typing import Iterable @@ -48,8 +50,6 @@ from .findpaths import determine_setup from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter -from _pytest.compat import final -from _pytest.compat import importlib_metadata # type: ignore[attr-defined] from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath @@ -257,7 +257,8 @@ default_plugins = essential_plugins + ( "logging", "reports", "python_path", - *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), + "unraisableexception", + "threadexception", "faulthandler", ) @@ -1216,7 +1217,7 @@ class Config: package_files = ( str(file) - for dist in importlib_metadata.distributions() + for dist in importlib.metadata.distributions() if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] ) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d3f01916b..108349f10 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -7,6 +7,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import List from typing import Mapping from typing import NoReturn @@ -17,7 +18,6 @@ from typing import TYPE_CHECKING from typing import Union import _pytest._io -from _pytest.compat import final from _pytest.config.exceptions import UsageError from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT from _pytest.deprecated import ARGUMENT_TYPE_STR diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 4f1320e75..4031ea732 100644 --- a/src/_pytest/config/exceptions.py +++ b/src/_pytest/config/exceptions.py @@ -1,4 +1,4 @@ -from _pytest.compat import final +from typing import final @final diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 66722da27..6e1134928 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -13,6 +13,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generator from typing import Generic from typing import Iterable @@ -21,6 +22,7 @@ from typing import List from typing import MutableMapping from typing import NoReturn from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple @@ -35,10 +37,8 @@ from _pytest._code import getfslineno from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames @@ -47,7 +47,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import NotSetType -from _pytest.compat import overload from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config @@ -729,8 +728,10 @@ class FixtureRequest: p = bestrelpath(session.path, fs) else: p = fs - args = _format_args(factory) - lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) + lines.append( + "%s:%d: def %s%s" + % (p, lineno + 1, factory.__name__, inspect.signature(factory)) + ) return lines def __repr__(self) -> str: diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index af1d0c07e..7091f038a 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -3,6 +3,7 @@ import dataclasses import shlex import subprocess from pathlib import Path +from typing import final from typing import List from typing import Optional from typing import TYPE_CHECKING @@ -11,7 +12,6 @@ from typing import Union from iniconfig import SectionWrapper from _pytest.cacheprovider import Cache -from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.config import Config diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 838134660..ea856837c 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -13,6 +13,7 @@ from logging import LogRecord from pathlib import Path from typing import AbstractSet from typing import Dict +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -25,7 +26,6 @@ from typing import Union from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager -from _pytest.compat import final from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 155d4300e..f2327ca68 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -9,10 +9,12 @@ import sys from pathlib import Path from typing import Callable from typing import Dict +from typing import final from typing import FrozenSet from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple @@ -22,8 +24,6 @@ from typing import Union import _pytest._code from _pytest import nodes -from _pytest.compat import final -from _pytest.compat import overload from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 9287bcee5..b995518bf 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -18,7 +18,6 @@ import ast import dataclasses import enum import re -import sys import types from typing import Callable from typing import Iterator @@ -27,12 +26,6 @@ from typing import NoReturn from typing import Optional from typing import Sequence -if sys.version_info >= (3, 8): - astNameConstant = ast.Constant -else: - astNameConstant = ast.NameConstant - - __all__ = [ "Expression", "ParseError", @@ -138,7 +131,7 @@ IDENT_PREFIX = "$" def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - ret: ast.expr = astNameConstant(False) + ret: ast.expr = ast.Constant(False) else: ret = expr(s) s.accept(TokenType.EOF, reject=True) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 406da5d9d..bc10d3b90 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -5,6 +5,7 @@ import warnings from typing import Any from typing import Callable from typing import Collection +from typing import final from typing import Iterable from typing import Iterator from typing import List @@ -23,7 +24,6 @@ from typing import Union from .._code import getfslineno from ..compat import ascii_escaped -from ..compat import final from ..compat import NOTSET from ..compat import NotSetType from _pytest.config import Config diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 9e51ff335..834700b1b 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -5,6 +5,7 @@ import sys import warnings from contextlib import contextmanager from typing import Any +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -15,7 +16,6 @@ from typing import Tuple from typing import TypeVar from typing import Union -from _pytest.compat import final from _pytest.fixtures import fixture from _pytest.warning_types import PytestWarning diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index dbd6b0a42..83180c126 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,5 +1,6 @@ import os import warnings +from functools import cached_property from inspect import signature from pathlib import Path from typing import Any @@ -23,7 +24,6 @@ from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest._code.code import Traceback -from _pytest.compat import cached_property from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 1be97dda4..7dab4499b 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -7,23 +7,12 @@ from typing import Callable from typing import cast from typing import NoReturn from typing import Optional +from typing import Protocol from typing import Type from typing import TypeVar from _pytest.deprecated import KEYWORD_MSG_ARG -TYPE_CHECKING = False # Avoid circular import through compat. - -if TYPE_CHECKING: - from typing_extensions import Protocol -else: - # typing.Protocol is only available starting from Python 3.8. It is also - # available from typing_extensions, but we don't want a runtime dependency - # on that. So use a dummy runtime implementation. - from typing import Generic - - Protocol = Generic - class OutcomeException(BaseException): """OutcomeException and its subclass instances indicate and contain info diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 3df52ebe8..0634267ae 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -20,6 +20,7 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import final from typing import Generator from typing import IO from typing import Iterable @@ -40,7 +41,6 @@ from iniconfig import SectionWrapper from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture -from _pytest.compat import final from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 6ba568c0f..5f4ba3da6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -15,6 +15,7 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import Iterator @@ -40,7 +41,6 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func from _pytest.compat import getimfunc diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 4213bd098..0967ae8ec 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -9,9 +9,11 @@ from typing import Any from typing import Callable from typing import cast from typing import ContextManager +from typing import final from typing import List from typing import Mapping from typing import Optional +from typing import overload from typing import Pattern from typing import Sequence from typing import Tuple @@ -20,17 +22,14 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import _pytest._code +from _pytest.compat import STRING_TYPES +from _pytest.outcomes import fail + if TYPE_CHECKING: from numpy import ndarray -import _pytest._code -from _pytest.compat import final -from _pytest.compat import STRING_TYPES -from _pytest.compat import overload -from _pytest.outcomes import fail - - def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = f" at {at}" if at else "" return TypeError( diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index d76ea020f..b422f3627 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -5,18 +5,18 @@ from pprint import pformat from types import TracebackType from typing import Any from typing import Callable +from typing import final from typing import Generator from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Pattern from typing import Tuple from typing import Type from typing import TypeVar from typing import Union -from _pytest.compat import final -from _pytest.compat import overload from _pytest.deprecated import check_ispytest from _pytest.deprecated import WARNS_NONE_ARG from _pytest.fixtures import fixture diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 74e8794b2..0a4044ec6 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -5,6 +5,7 @@ from pprint import pprint from typing import Any from typing import cast from typing import Dict +from typing import final from typing import Iterable from typing import Iterator from typing import List @@ -29,7 +30,6 @@ from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f861c05a4..1f14ff944 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -6,6 +6,7 @@ import sys from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generic from typing import List from typing import Optional @@ -23,7 +24,6 @@ from _pytest import timing from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr -from _pytest.compat import final from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.nodes import Collector diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b0cdb58ce..089314c3e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -18,6 +18,7 @@ from typing import Callable from typing import cast from typing import ClassVar from typing import Dict +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -40,7 +41,6 @@ from _pytest._code.code import ExceptionRepr from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.assertion.util import running_on_ci -from _pytest.compat import final from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 3cc2bace5..fe0855c18 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -7,38 +7,32 @@ from pathlib import Path from shutil import rmtree from typing import Any from typing import Dict +from typing import final from typing import Generator +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING from typing import Union -from _pytest.nodes import Item -from _pytest.reports import CollectReport -from _pytest.stash import StashKey - -if TYPE_CHECKING: - from typing_extensions import Literal - - RetentionType = Literal["all", "failed", "none"] - - -from _pytest.config.argparsing import Parser - +from .pathlib import cleanup_dead_symlinks from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf -from .pathlib import cleanup_dead_symlinks -from _pytest.compat import final, get_user_id +from _pytest.compat import get_user_id from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item +from _pytest.reports import CollectReport +from _pytest.stash import StashKey tmppath_result_key = StashKey[Dict[str, bool]]() +RetentionType = Literal["all", "failed", "none"] @final diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index f6b0a3a69..31726e1ce 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -3,12 +3,11 @@ import inspect import warnings from types import FunctionType from typing import Any +from typing import final from typing import Generic from typing import Type from typing import TypeVar -from _pytest.compat import final - class PytestWarning(UserWarning): """Base class for all warnings emitted by pytest.""" diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index de9e92d00..453e26972 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,10 +1,10 @@ import dataclasses +import importlib.metadata import os import sys import types import pytest -from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester @@ -139,7 +139,7 @@ class TestGeneralUsage: def my_dists(): return (DummyDist(entry_points),) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) params = ("-p", "mycov") if load_cov_early else () pytester.runpytest_inprocess(*params) if load_cov_early: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index dc35c9496..260c80299 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -439,14 +439,9 @@ comment 4 ''' for line in range(2, 6): assert str(getstatement(line, source)) == " x = 1" - if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - tqs_start = 8 - else: - tqs_start = 10 - assert str(getstatement(10, source)) == '"""' - for line in range(6, tqs_start): + for line in range(6, 8): assert str(getstatement(line, source)) == " assert False" - for line in range(tqs_start, 10): + for line in range(8, 10): assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index a9e9b5269..08ea8f910 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -19,7 +19,6 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python -from _pytest.compat import _format_args from _pytest.compat import getfuncargnames from _pytest.compat import NOTSET from _pytest.outcomes import fail @@ -1036,27 +1035,6 @@ class TestMetafunc: """ ) - def test_format_args(self) -> None: - def function1(): - pass - - assert _format_args(function1) == "()" - - def function2(arg1): - pass - - assert _format_args(function2) == "(arg1)" - - def function3(arg1, arg2="qwe"): - pass - - assert _format_args(function3) == "(arg1, arg2='qwe')" - - def function4(arg1, *args, **kwargs): - pass - - assert _format_args(function4) == "(arg1, *args, **kwargs)" - class TestMetafuncFunctional: def test_attributes(self, pytester: Pytester) -> None: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 7119b3b5a..c04c31f31 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -199,8 +199,8 @@ class TestImportHookInstallation: return check """, "mainwrapper.py": """\ + import importlib.metadata import pytest - from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' @@ -220,7 +220,7 @@ class TestImportHookInstallation: def distributions(): return (DummyDistInfo(),) - importlib_metadata.distributions = distributions + importlib.metadata.distributions = distributions pytest.main() """, "test_foo.py": """\ diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 778f843e6..d01803204 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -131,9 +131,8 @@ class TestAssertionRewrite: for n in [node, *ast.iter_child_nodes(node)]: assert n.lineno == 3 assert n.col_offset == 0 - if sys.version_info >= (3, 8): - assert n.end_lineno == 6 - assert n.end_col_offset == 3 + assert n.end_lineno == 6 + assert n.end_col_offset == 3 def test_dont_rewrite(self) -> None: s = """'PYTEST_DONT_REWRITE'\nassert 14""" @@ -1270,9 +1269,6 @@ class TestIssue2121: result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="walrus operator not available in py<38" -) class TestIssue10743: def test_assertion_walrus_operator(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1441,9 +1437,6 @@ class TestIssue10743: assert result.ret == 0 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="walrus operator not available in py<38" -) class TestIssue11028: def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1957,16 +1950,10 @@ class TestPyCacheDir: ) def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None: monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - - if prefix is not None and sys.version_info < (3, 8): - pytest.skip("pycache_prefix not available in py<38") monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False) assert get_cache_dir(Path(source)) == Path(expected) - @pytest.mark.skipif( - sys.version_info < (3, 8), reason="pycache_prefix not available in py<38" - ) @pytest.mark.skipif( sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"), reason="#9298", diff --git a/testing/test_compat.py b/testing/test_compat.py index 8a80fd625..27c6db95b 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,5 +1,6 @@ import enum import sys +from functools import cached_property from functools import partial from functools import wraps from typing import TYPE_CHECKING @@ -8,7 +9,6 @@ from typing import Union import pytest from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr diff --git a/testing/test_config.py b/testing/test_config.py index 257d696fa..9b3fe4af0 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,4 +1,5 @@ import dataclasses +import importlib.metadata import os import re import sys @@ -13,7 +14,6 @@ from typing import Union import _pytest._code import pytest -from _pytest.compat import importlib_metadata from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool @@ -475,7 +475,7 @@ class TestParseIni: pytester.makepyfile(myplugin1_module="# my plugin module") pytester.syspathinsert() - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) pytester.makeini(ini_file_text) @@ -1003,7 +1003,7 @@ def test_preparse_ordering_with_setuptools( def my_dists(): return (Dist,) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) pytester.makeconftest( """ pytest_plugins = "mytestplugin", @@ -1036,7 +1036,7 @@ def test_setuptools_importerror_issue1479( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) with pytest.raises(ImportError): pytester.parseconfig() @@ -1063,7 +1063,7 @@ def test_importlib_metadata_broken_distribution( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) pytester.parseconfig() @@ -1091,7 +1091,7 @@ def test_plugin_preparse_prevents_setuptools_loading( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () config = pytester.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") @@ -1140,7 +1140,7 @@ def test_disable_plugin_autoload( return (Distribution(),) monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index 5d0031273..dfb3d57d2 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -1,7 +1,7 @@ -from _pytest.compat import importlib_metadata +import importlib.metadata def test_pytest_entry_points_are_identical(): - dist = importlib_metadata.distribution("pytest") + dist = importlib.metadata.distribution("pytest") entry_map = {ep.name: ep for ep in dist.entry_points} assert entry_map["pytest"].value == entry_map["py.test"].value diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 6b8034610..d8b22aa46 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1142,12 +1142,10 @@ def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None: """ ) result = pytester.runpytest() - markline = " ^" + markline = " ^" pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info is not None and pypy_version_info < (6,): - markline = markline[5:] - elif sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - markline = markline[4:] + markline = markline[1:] if sys.version_info[:2] >= (3, 10): expected = [ diff --git a/testing/test_threadexception.py b/testing/test_threadexception.py index 5b7519f27..fd9a091cc 100644 --- a/testing/test_threadexception.py +++ b/testing/test_threadexception.py @@ -1,13 +1,7 @@ -import sys - import pytest from _pytest.pytester import Pytester -if sys.version_info < (3, 8): - pytest.skip("threadexception plugin needs Python>=3.8", allow_module_level=True) - - @pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning") def test_unhandled_thread_exception(pytester: Pytester) -> None: pytester.makepyfile( diff --git a/testing/test_unittest.py b/testing/test_unittest.py index d917d331a..99a53e0a9 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1354,9 +1354,6 @@ def test_plain_unittest_does_not_support_async(pytester: Pytester) -> None: result.stdout.fnmatch_lines(expected_lines) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_success(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1382,9 +1379,6 @@ def test_do_class_cleanups_on_success(pytester: Pytester) -> None: assert passed == 3 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1409,9 +1403,6 @@ def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: assert passed == 1 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_teardownclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index f625833dc..72f620364 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -1,13 +1,7 @@ -import sys - import pytest from _pytest.pytester import Pytester -if sys.version_info < (3, 8): - pytest.skip("unraisableexception plugin needs Python>=3.8", allow_module_level=True) - - @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable(pytester: Pytester) -> None: pytester.makepyfile( From f617bab0a243dea5ffe6c61ca21f280c56d9280c Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 15:07:07 -0700 Subject: [PATCH 10/53] Update pre-commit config --- .pre-commit-config.yaml | 4 ++-- src/_pytest/_code/code.py | 23 ++++++++++------------- src/_pytest/capture.py | 12 +++++------- src/_pytest/compat.py | 5 +---- src/_pytest/legacypath.py | 3 +-- src/_pytest/pytester.py | 6 ++---- 6 files changed, 21 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f1cd6743..94ec67408 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,12 +40,12 @@ repos: rev: v3.10.0 hooks: - id: reorder-python-imports - args: ['--application-directories=.:src', --py37-plus] + args: ['--application-directories=.:src', --py38-plus] - repo: https://github.com/asottile/pyupgrade rev: v3.7.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/asottile/setup-cfg-fmt rev: v2.3.0 hooks: diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index ddc03d752..58483d2a0 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -17,19 +17,21 @@ from typing import Any from typing import Callable from typing import ClassVar from typing import Dict +from typing import Final from typing import final from typing import Generic from typing import Iterable from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import overload from typing import Pattern from typing import Sequence from typing import Set +from typing import SupportsIndex from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -48,16 +50,11 @@ from _pytest.deprecated import check_ispytest from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath -if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - from typing_extensions import SupportsIndex - - _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] - if sys.version_info[:2] < (3, 11): from exceptiongroup import BaseExceptionGroup +_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] + class Code: """Wrapper around Python code objects.""" @@ -633,7 +630,7 @@ class ExceptionInfo(Generic[E]): def getrepr( self, showlocals: bool = False, - style: "_TracebackStyle" = "long", + style: _TracebackStyle = "long", abspath: bool = False, tbfilter: Union[ bool, Callable[["ExceptionInfo[BaseException]"], Traceback] @@ -725,7 +722,7 @@ class FormattedExcinfo: fail_marker: ClassVar = "E" showlocals: bool = False - style: "_TracebackStyle" = "long" + style: _TracebackStyle = "long" abspath: bool = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True funcargs: bool = False @@ -1090,7 +1087,7 @@ class ReprExceptionInfo(ExceptionRepr): class ReprTraceback(TerminalRepr): reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] extraline: Optional[str] - style: "_TracebackStyle" + style: _TracebackStyle entrysep: ClassVar = "_ " @@ -1124,7 +1121,7 @@ class ReprTracebackNative(ReprTraceback): class ReprEntryNative(TerminalRepr): lines: Sequence[str] - style: ClassVar["_TracebackStyle"] = "native" + style: ClassVar[_TracebackStyle] = "native" def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) @@ -1136,7 +1133,7 @@ class ReprEntry(TerminalRepr): reprfuncargs: Optional["ReprFuncArgs"] reprlocals: Optional["ReprLocals"] reprfileloc: Optional["ReprFileLocation"] - style: "_TracebackStyle" + style: _TracebackStyle def _write_entry_lines(self, tw: TerminalWriter) -> None: """Write the source code portions of a list of traceback entries with syntax highlighting. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3e31e3cb7..5c62cf54d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -11,12 +11,14 @@ from types import TracebackType from typing import Any from typing import AnyStr from typing import BinaryIO +from typing import Final from typing import final from typing import Generator from typing import Generic from typing import Iterable from typing import Iterator from typing import List +from typing import Literal from typing import NamedTuple from typing import Optional from typing import TextIO @@ -35,11 +37,7 @@ from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item -if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - - _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] +_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] def pytest_addoption(parser: Parser) -> None: @@ -687,7 +685,7 @@ class MultiCapture(Generic[AnyStr]): return CaptureResult(out, err) # type: ignore[arg-type] -def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: +def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]: if method == "fd": return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) elif method == "sys": @@ -723,7 +721,7 @@ class CaptureManager: needed to ensure the fixtures take precedence over the global capture. """ - def __init__(self, method: "_CaptureMethod") -> None: + def __init__(self, method: _CaptureMethod) -> None: self._method: Final = method self._global_capturing: Optional[MultiCapture[str]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 4736bf40d..998f540f3 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -12,15 +12,12 @@ from inspect import signature from pathlib import Path from typing import Any from typing import Callable +from typing import Final from typing import NoReturn -from typing import TYPE_CHECKING from typing import TypeVar import py -if TYPE_CHECKING: - from typing_extensions import Final - _T = TypeVar("_T") _S = TypeVar("_S") diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 7091f038a..8df0a5163 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -3,6 +3,7 @@ import dataclasses import shlex import subprocess from pathlib import Path +from typing import Final from typing import final from typing import List from typing import Optional @@ -32,8 +33,6 @@ from _pytest.terminal import TerminalReporter from _pytest.tmpdir import TempPathFactory if TYPE_CHECKING: - from typing_extensions import Final - import pexpect diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0634267ae..0129c224f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -20,11 +20,13 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import Final from typing import final from typing import Generator from typing import IO from typing import Iterable from typing import List +from typing import Literal from typing import Optional from typing import overload from typing import Sequence @@ -68,11 +70,7 @@ from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestWarning - if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - import pexpect From 165fbbd12a74ab639d61ce1f28dfef1511a2c2e2 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 15:19:19 -0700 Subject: [PATCH 11/53] Drop py37 from CI --- .github/workflows/deploy.yml | 2 +- .github/workflows/test.yml | 55 ++++++++++++------------------ CONTRIBUTING.rst | 16 ++++----- README.rst | 2 +- doc/en/backwards-compatibility.rst | 1 + doc/en/getting-started.rst | 2 +- doc/en/index.rst | 2 +- tox.ini | 9 +++-- 8 files changed, 39 insertions(+), 50 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 252809946..186adc55a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -45,7 +45,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.11" - name: Install tox run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf5027223..68f05a58a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,25 +37,23 @@ jobs: fail-fast: false matrix: name: [ - "windows-py37", - "windows-py37-pluggy", "windows-py38", + "windows-py38-pluggy", "windows-py39", "windows-py310", "windows-py311", "windows-py312", - "ubuntu-py37", - "ubuntu-py37-pluggy", - "ubuntu-py37-freeze", "ubuntu-py38", + "ubuntu-py38-pluggy", + "ubuntu-py38-freeze", "ubuntu-py39", "ubuntu-py310", "ubuntu-py311", "ubuntu-py312", "ubuntu-pypy3", - "macos-py37", + "macos-py38", "macos-py39", "macos-py310", "macos-py312", @@ -66,19 +64,15 @@ jobs: ] include: - - name: "windows-py37" - python: "3.7" - os: windows-latest - tox_env: "py37-numpy" - - name: "windows-py37-pluggy" - python: "3.7" - os: windows-latest - tox_env: "py37-pluggymain-pylib-xdist" - name: "windows-py38" python: "3.8" os: windows-latest tox_env: "py38-unittestextras" use_coverage: true + - name: "windows-py38-pluggy" + python: "3.8" + os: windows-latest + tox_env: "py38-pluggymain-pylib-xdist" - name: "windows-py39" python: "3.9" os: windows-latest @@ -96,23 +90,18 @@ jobs: os: windows-latest tox_env: "py312" - - name: "ubuntu-py37" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-lsof-numpy-pexpect" - use_coverage: true - - name: "ubuntu-py37-pluggy" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-pluggymain-pylib-xdist" - - name: "ubuntu-py37-freeze" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-freeze" - name: "ubuntu-py38" python: "3.8" os: ubuntu-latest tox_env: "py38-xdist" + - name: "ubuntu-py38-pluggy" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-pluggymain-pylib-xdist" + - name: "ubuntu-py38-freeze" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-freeze" - name: "ubuntu-py39" python: "3.9" os: ubuntu-latest @@ -132,14 +121,14 @@ jobs: tox_env: "py312" use_coverage: true - name: "ubuntu-pypy3" - python: "pypy-3.7" + python: "pypy-3.8" os: ubuntu-latest tox_env: "pypy3-xdist" - - name: "macos-py37" - python: "3.7" + - name: "macos-py38" + python: "3.8" os: macos-latest - tox_env: "py37-xdist" + tox_env: "py38-xdist" - name: "macos-py39" python: "3.9" os: macos-latest @@ -160,11 +149,11 @@ jobs: tox_env: "plugins" - name: "docs" - python: "3.7" + python: "3.8" os: ubuntu-latest tox_env: "docs" - name: "doctesting" - python: "3.7" + python: "3.8" os: ubuntu-latest tox_env: "doctesting" use_coverage: true diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 791f98830..0f6d54351 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -201,7 +201,7 @@ Short version #. Follow **PEP-8** for naming and `black `_ for formatting. #. Tests are run using ``tox``:: - tox -e linting,py37 + tox -e linting,py39 The test environments above are usually enough to cover most cases locally. @@ -272,24 +272,24 @@ Here is a simple overview, with pytest-specific bits: #. Run all the tests - You need to have Python 3.7 available in your system. Now + You need to have Python 3.8 or later available in your system. Now running tests is as simple as issuing this command:: - $ tox -e linting,py37 + $ tox -e linting,py39 - This command will run tests via the "tox" tool against Python 3.7 + This command will run tests via the "tox" tool against Python 3.9 and also perform "lint" coding-style checks. #. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. - You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest + You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest (e.g. enter pdb on failure) to pytest you can do:: - $ tox -e py37 -- --pdb + $ tox -e py39 -- --pdb - Or to only run tests in a particular test module on Python 3.7:: + Or to only run tests in a particular test module on Python 3.9:: - $ tox -e py37 -- testing/test_config.py + $ tox -e py39 -- testing/test_config.py When committing, ``pre-commit`` will re-format the files if necessary. diff --git a/README.rst b/README.rst index 034034a40..e6bb6d4cf 100644 --- a/README.rst +++ b/README.rst @@ -100,7 +100,7 @@ Features - Can run `unittest `_ (or trial), `nose `_ test suites out of the box -- Python 3.7+ or PyPy3 +- Python 3.8+ or PyPy3 - Rich plugin architecture, with over 850+ `external plugins `_ and thriving community diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index ea0c6a71a..4ffb9fe97 100644 --- a/doc/en/backwards-compatibility.rst +++ b/doc/en/backwards-compatibility.rst @@ -87,6 +87,7 @@ Released pytest versions support all Python versions that are actively maintaine ============== =================== pytest version min. Python version ============== =================== +8.0+ 3.8+ 7.1+ 3.7+ 6.2 - 7.0 3.6+ 5.0 - 6.1 3.5+ diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index e295c1804..4a9dc4522 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -9,7 +9,7 @@ Get Started Install ``pytest`` ---------------------------------------- -``pytest`` requires: Python 3.7+ or PyPy3. +``pytest`` requires: Python 3.8+ or PyPy3. 1. Run the following command in your command line: diff --git a/doc/en/index.rst b/doc/en/index.rst index de07831ac..23b6964c9 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -77,7 +77,7 @@ Features - Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box -- Python 3.7+ or PyPy 3 +- Python 3.8+ or PyPy 3 - Rich plugin architecture, with over 800+ :ref:`external plugins ` and thriving community diff --git a/tox.ini b/tox.ini index 05c1842af..9cdbfe3ab 100644 --- a/tox.ini +++ b/tox.ini @@ -4,17 +4,16 @@ minversion = 3.20.0 distshare = {homedir}/.tox/distshare envlist = linting - py37 py38 py39 py310 py311 py312 pypy3 - py37-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} + py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} doctesting plugins - py37-freeze + py38-freeze docs docs-checklinks @@ -43,7 +42,7 @@ setenv = PYTHONWARNDEFAULTENCODING=1 # Configuration to run with coverage similar to CI, e.g. - # "tox -e py37-coverage". + # "tox -e py38-coverage". coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess coverage: COVERAGE_FILE={toxinidir}/.coverage @@ -136,7 +135,7 @@ commands = pytest pytest_twisted_integration.py pytest simple_integration.py --force-sugar --flakes -[testenv:py37-freeze] +[testenv:py38-freeze] changedir = testing/freeze deps = pyinstaller From 9279ea2882b96be6f7de3f8fbaa38062b1f2353e Mon Sep 17 00:00:00 2001 From: Reagan Lee <96998476+reaganjlee@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:29:02 -0700 Subject: [PATCH 12/53] Emit unmatched warnings from pytest.warns() --- AUTHORS | 1 + changelog/9288.breaking.rst | 1 + src/_pytest/recwarn.py | 33 ++++++++++++++++++++++++++++++ testing/test_recwarn.py | 40 +++++++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 changelog/9288.breaking.rst diff --git a/AUTHORS b/AUTHORS index e0ed531b0..28116c3c8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -311,6 +311,7 @@ Raphael Pierzina Rafal Semik Raquel Alegre Ravi Chandra +Reagan Lee Robert Holt Roberto Aldera Roberto Polli diff --git a/changelog/9288.breaking.rst b/changelog/9288.breaking.rst new file mode 100644 index 000000000..7f101123d --- /dev/null +++ b/changelog/9288.breaking.rst @@ -0,0 +1 @@ +Set :func:`warns` to re-emit unmatched warnings when the context closes diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index b422f3627..334b3b850 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -149,6 +149,10 @@ def warns( # noqa: F811 This could be achieved in the same way as with exceptions, see :ref:`parametrizing_conditional_raising` for an example. + .. note:: + Unlike the stdlib :func:`warnings.catch_warnings` context manager, + unmatched warnings will be re-emitted when the context closes. + """ __tracebackhide__ = True if not args: @@ -290,6 +294,32 @@ class WarningsChecker(WarningsRecorder): def found_str(): return pformat([record.message for record in self], indent=2) + def re_emit() -> None: + for r in self: + if matches(r): + continue + + assert issubclass(r.message.__class__, Warning) + + warnings.warn_explicit( + str(r.message), + r.message.__class__, + r.filename, + r.lineno, + module=r.__module__, + source=r.source, + ) + + def matches(warning) -> bool: + if self.expected_warning is not None: + if issubclass(warning.category, self.expected_warning): + if self.match_expr is not None: + if re.compile(self.match_expr).search(str(warning.message)): + return True + return False + return True + return False + # only check if we're not currently handling an exception if exc_type is None and exc_val is None and exc_tb is None: if self.expected_warning is not None: @@ -303,6 +333,7 @@ class WarningsChecker(WarningsRecorder): for r in self: if issubclass(r.category, self.expected_warning): if re.compile(self.match_expr).search(str(r.message)): + re_emit() break else: fail( @@ -311,3 +342,5 @@ DID NOT WARN. No warnings of type {self.expected_warning} matching the regex wer Regex: {self.match_expr} Emitted warnings: {found_str()}""" ) + else: + re_emit() diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 7e0f836a6..d9cd1d778 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -376,10 +376,12 @@ class TestWarns: warnings.warn("value must be 42", UserWarning) def test_one_from_multiple_warns(self) -> None: - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("cccccccccc", UserWarning) - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("aaaaaaaaaa", UserWarning) + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r"aaa"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) def test_none_of_multiple_warns(self) -> None: with pytest.raises(pytest.fail.Exception): @@ -403,3 +405,33 @@ class TestWarns: with pytest.warns(UserWarning, foo="bar"): # type: ignore pass assert "Unexpected keyword arguments" in str(excinfo.value) + + def test_re_emit_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_multiple(self) -> None: + with pytest.warns(UserWarning): + warnings.warn("first warning", UserWarning) + warnings.warn("second warning", UserWarning) + + def test_re_emit_match_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_match_multiple(self) -> None: + # with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("first user warning", UserWarning) + warnings.warn("second user warning", UserWarning) + + def test_re_emit_non_match_single(self) -> None: + # with pytest.warns(UserWarning): + with pytest.warns(UserWarning, match="v2 warning"): + with pytest.warns(UserWarning, match="v1 warning"): + warnings.warn("v1 warning", UserWarning) + warnings.warn("non-matching v2 warning", UserWarning) From a1b37022afd4aec31009a3e56872c435b05e3c45 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 15:29:02 -0700 Subject: [PATCH 13/53] Refactor warns() exit logic --- src/_pytest/recwarn.py | 85 ++++++++++++++++++----------------------- testing/test_recwarn.py | 14 +++---- 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 334b3b850..5452b7cb9 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -281,6 +281,12 @@ class WarningsChecker(WarningsRecorder): self.expected_warning = expected_warning_tup self.match_expr = match_expr + def matches(self, warning: warnings.WarningMessage) -> bool: + assert self.expected_warning is not None + return issubclass(warning.category, self.expected_warning) and bool( + self.match_expr is None or re.search(self.match_expr, str(warning.message)) + ) + def __exit__( self, exc_type: Optional[Type[BaseException]], @@ -291,56 +297,39 @@ class WarningsChecker(WarningsRecorder): __tracebackhide__ = True + if self.expected_warning is None: + # nothing to do in this deprecated case, see WARNS_NONE_ARG above + return + + if not (exc_type is None and exc_val is None and exc_tb is None): + # We currently ignore missing warnings if an exception is active. + # TODO: fix this, because it means things are surprisingly order-sensitive. + return + def found_str(): return pformat([record.message for record in self], indent=2) - def re_emit() -> None: - for r in self: - if matches(r): - continue - - assert issubclass(r.message.__class__, Warning) - - warnings.warn_explicit( - str(r.message), - r.message.__class__, - r.filename, - r.lineno, - module=r.__module__, - source=r.source, + try: + if not any(issubclass(w.category, self.expected_warning) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" + f" Emitted warnings: {found_str()}." ) - - def matches(warning) -> bool: - if self.expected_warning is not None: - if issubclass(warning.category, self.expected_warning): - if self.match_expr is not None: - if re.compile(self.match_expr).search(str(warning.message)): - return True - return False - return True - return False - - # only check if we're not currently handling an exception - if exc_type is None and exc_val is None and exc_tb is None: - if self.expected_warning is not None: - if not any(issubclass(r.category, self.expected_warning) for r in self): - __tracebackhide__ = True - fail( - f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" - f"The list of emitted warnings is: {found_str()}." + elif not any(self.matches(w) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n" + f" Regex: {self.match_expr}\n" + f" Emitted warnings: {found_str()}." + ) + finally: + # Whether or not any warnings matched, we want to re-emit all unmatched warnings. + for w in self: + if not self.matches(w): + warnings.warn_explicit( + str(w.message), + w.message.__class__, + w.filename, + w.lineno, + module=w.__module__, + source=w.source, ) - elif self.match_expr is not None: - for r in self: - if issubclass(r.category, self.expected_warning): - if re.compile(self.match_expr).search(str(r.message)): - re_emit() - break - else: - fail( - f"""\ -DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted. - Regex: {self.match_expr} - Emitted warnings: {found_str()}""" - ) - else: - re_emit() diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index d9cd1d778..a423b2001 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -376,7 +376,7 @@ class TestWarns: warnings.warn("value must be 42", UserWarning) def test_one_from_multiple_warns(self) -> None: - with pytest.raises(pytest.fail.Exception): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): with pytest.warns(UserWarning, match=r"aaa"): with pytest.warns(UserWarning, match=r"aaa"): warnings.warn("cccccccccc", UserWarning) @@ -384,7 +384,7 @@ class TestWarns: warnings.warn("aaaaaaaaaa", UserWarning) def test_none_of_multiple_warns(self) -> None: - with pytest.raises(pytest.fail.Exception): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): with pytest.warns(UserWarning, match=r"aaa"): warnings.warn("bbbbbbbbbb", UserWarning) warnings.warn("cccccccccc", UserWarning) @@ -424,13 +424,13 @@ class TestWarns: warnings.warn("some deprecation warning", DeprecationWarning) def test_re_emit_match_multiple(self) -> None: - # with pytest.warns(UserWarning): - with pytest.warns(UserWarning, match="user warning"): - warnings.warn("first user warning", UserWarning) - warnings.warn("second user warning", UserWarning) + with warnings.catch_warnings(): + warnings.simplefilter("error") # if anything is re-emitted + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("first user warning", UserWarning) + warnings.warn("second user warning", UserWarning) def test_re_emit_non_match_single(self) -> None: - # with pytest.warns(UserWarning): with pytest.warns(UserWarning, match="v2 warning"): with pytest.warns(UserWarning, match="v1 warning"): warnings.warn("v1 warning", UserWarning) From 7022fb455d5c5853cb839cd20e53bbbacaa46d28 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 15:29:03 -0700 Subject: [PATCH 14/53] Update tests for re-emitted warnings --- src/_pytest/recwarn.py | 7 +-- testing/test_recwarn.py | 103 ++++++++++++++++++++++------------------ 2 files changed, 61 insertions(+), 49 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 5452b7cb9..efe2a3dc6 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -135,8 +135,9 @@ def warns( # noqa: F811 >>> with pytest.warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("value must be 42", UserWarning) - >>> with pytest.warns(UserWarning, match=r'must be \d+$'): - ... warnings.warn("this is not here", UserWarning) + >>> with pytest.warns(UserWarning): # catch re-emitted warning + ... with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) Traceback (most recent call last): ... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... @@ -327,7 +328,7 @@ class WarningsChecker(WarningsRecorder): if not self.matches(w): warnings.warn_explicit( str(w.message), - w.message.__class__, + w.message.__class__, # type: ignore w.filename, w.lineno, module=w.__module__, diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index a423b2001..6b3856bd9 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -203,19 +203,21 @@ class TestDeprecatedCall: def f(): warnings.warn(warning("hi")) - with pytest.raises(pytest.fail.Exception): - pytest.deprecated_call(f) - with pytest.raises(pytest.fail.Exception): - with pytest.deprecated_call(): - f() + with pytest.warns(warning): + with pytest.raises(pytest.fail.Exception): + pytest.deprecated_call(f) + with pytest.raises(pytest.fail.Exception): + with pytest.deprecated_call(): + f() def test_deprecated_call_supports_match(self) -> None: with pytest.deprecated_call(match=r"must be \d+$"): warnings.warn("value must be 42", DeprecationWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.deprecated_call(match=r"must be \d+$"): - warnings.warn("this is not here", DeprecationWarning) + with pytest.deprecated_call(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("this is not here", DeprecationWarning) class TestWarns: @@ -227,8 +229,9 @@ class TestWarns: def test_several_messages(self) -> None: # different messages, b/c Python suppresses multiple identical warnings pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning)) - with pytest.raises(pytest.fail.Exception): - pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) + with pytest.warns(RuntimeWarning): + with pytest.raises(pytest.fail.Exception): + pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning)) def test_function(self) -> None: @@ -243,13 +246,14 @@ class TestWarns: pytest.warns( (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning) ) - pytest.raises( - pytest.fail.Exception, - lambda: pytest.warns( - (RuntimeWarning, SyntaxWarning), - lambda: warnings.warn("w3", UserWarning), - ), - ) + with pytest.warns(): + pytest.raises( + pytest.fail.Exception, + lambda: pytest.warns( + (RuntimeWarning, SyntaxWarning), + lambda: warnings.warn("w3", UserWarning), + ), + ) def test_as_contextmanager(self) -> None: with pytest.warns(RuntimeWarning): @@ -258,20 +262,22 @@ class TestWarns: with pytest.warns(UserWarning): warnings.warn("user", UserWarning) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(RuntimeWarning): - warnings.warn("user", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(RuntimeWarning): + warnings.warn("user", UserWarning) excinfo.match( r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n" - r"The list of emitted warnings is: \[UserWarning\('user',?\)\]." + r" Emitted warnings: \[UserWarning\('user',?\)\]." ) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(UserWarning): - warnings.warn("runtime", RuntimeWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(UserWarning): + warnings.warn("runtime", RuntimeWarning) excinfo.match( r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n" - r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)]." + r" Emitted warnings: \[RuntimeWarning\('runtime',?\)]." ) with pytest.raises(pytest.fail.Exception) as excinfo: @@ -279,19 +285,20 @@ class TestWarns: pass excinfo.match( r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n" - r"The list of emitted warnings is: \[\]." + r" Emitted warnings: \[\]." ) warning_classes = (UserWarning, FutureWarning) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(warning_classes) as warninfo: - warnings.warn("runtime", RuntimeWarning) - warnings.warn("import", ImportWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(warning_classes) as warninfo: + warnings.warn("runtime", RuntimeWarning) + warnings.warn("import", ImportWarning) messages = [each.message for each in warninfo] expected_str = ( f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n" - f"The list of emitted warnings is: {messages}." + f" Emitted warnings: {messages}." ) assert str(excinfo.value) == expected_str @@ -367,27 +374,31 @@ class TestWarns: with pytest.warns(UserWarning, match=r"must be \d+$"): warnings.warn("value must be 42", UserWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.warns(UserWarning, match=r"must be \d+$"): - warnings.warn("this is not here", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r"must be \d+$"): + warnings.warn("this is not here", UserWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.warns(FutureWarning, match=r"must be \d+$"): - warnings.warn("value must be 42", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(FutureWarning, match=r"must be \d+$"): + warnings.warn("value must be 42", UserWarning) def test_one_from_multiple_warns(self) -> None: - with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): - with pytest.warns(UserWarning, match=r"aaa"): + with pytest.warns(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("cccccccccc", UserWarning) - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("aaaaaaaaaa", UserWarning) + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) def test_none_of_multiple_warns(self) -> None: - with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("cccccccccc", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("cccccccccc", UserWarning) @pytest.mark.filterwarnings("ignore") def test_can_capture_previously_warned(self) -> None: From 2d48171e88e861bde0e7cf3d3d087ebbe203c103 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 15:29:03 -0700 Subject: [PATCH 15/53] Tweak docs on review --- changelog/9288.breaking.rst | 8 +++++++- src/_pytest/recwarn.py | 12 ++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/changelog/9288.breaking.rst b/changelog/9288.breaking.rst index 7f101123d..053af8013 100644 --- a/changelog/9288.breaking.rst +++ b/changelog/9288.breaking.rst @@ -1 +1,7 @@ -Set :func:`warns` to re-emit unmatched warnings when the context closes +:func:`pytest.warns ` now re-emits unmatched warnings when the context +closes -- previously it would consume all warnings, hiding those that were not +matched by the function. + +While this is a new feature, we decided to announce this as a breaking change +because many test suites are configured to error-out on warnings, and will +therefore fail on the newly-re-emitted warnings. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index efe2a3dc6..127a7a856 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -117,10 +117,10 @@ def warns( # noqa: F811 warning of that class or classes. This helper produces a list of :class:`warnings.WarningMessage` objects, one for - each warning raised (regardless of whether it is an ``expected_warning`` or not). + each warning emitted (regardless of whether it is an ``expected_warning`` or not). + Since pytest 8.0, unmatched warnings are also re-emitted when the context closes. - This function can be used as a context manager, which will capture all the raised - warnings inside it:: + This function can be used as a context manager:: >>> import pytest >>> with pytest.warns(RuntimeWarning): @@ -150,10 +150,6 @@ def warns( # noqa: F811 This could be achieved in the same way as with exceptions, see :ref:`parametrizing_conditional_raising` for an example. - .. note:: - Unlike the stdlib :func:`warnings.catch_warnings` context manager, - unmatched warnings will be re-emitted when the context closes. - """ __tracebackhide__ = True if not args: @@ -328,7 +324,7 @@ class WarningsChecker(WarningsRecorder): if not self.matches(w): warnings.warn_explicit( str(w.message), - w.message.__class__, # type: ignore + w.message.__class__, # type: ignore[arg-type] w.filename, w.lineno, module=w.__module__, From 15524f34d2aa8a0b311ea1ce735d9b6b471b8018 Mon Sep 17 00:00:00 2001 From: Cheukting Date: Thu, 22 Jun 2023 16:48:20 +0100 Subject: [PATCH 16/53] capture warning when exception is raised (fix #9036) --- changelog/9036.bugfix.rst | 1 + src/_pytest/recwarn.py | 5 ----- testing/test_recwarn.py | 28 ++++++++++++---------------- 3 files changed, 13 insertions(+), 21 deletions(-) create mode 100644 changelog/9036.bugfix.rst diff --git a/changelog/9036.bugfix.rst b/changelog/9036.bugfix.rst new file mode 100644 index 000000000..4f25f82e2 --- /dev/null +++ b/changelog/9036.bugfix.rst @@ -0,0 +1 @@ +``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block. diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 127a7a856..ff8e70820 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -298,11 +298,6 @@ class WarningsChecker(WarningsRecorder): # nothing to do in this deprecated case, see WARNS_NONE_ARG above return - if not (exc_type is None and exc_val is None and exc_tb is None): - # We currently ignore missing warnings if an exception is active. - # TODO: fix this, because it means things are surprisingly order-sensitive. - return - def found_str(): return pformat([record.message for record in self], indent=2) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 6b3856bd9..16b8d5443 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -172,22 +172,6 @@ class TestDeprecatedCall: with pytest.deprecated_call(): assert f() == 10 - @pytest.mark.parametrize("mode", ["context_manager", "call"]) - def test_deprecated_call_exception_is_raised(self, mode) -> None: - """If the block of the code being tested by deprecated_call() raises an exception, - it must raise the exception undisturbed. - """ - - def f(): - raise ValueError("some exception") - - with pytest.raises(ValueError, match="some exception"): - if mode == "call": - pytest.deprecated_call(f) - else: - with pytest.deprecated_call(): - f() - def test_deprecated_call_specificity(self) -> None: other_warnings = [ Warning, @@ -446,3 +430,15 @@ class TestWarns: with pytest.warns(UserWarning, match="v1 warning"): warnings.warn("v1 warning", UserWarning) warnings.warn("non-matching v2 warning", UserWarning) + + def test_catch_warning_within_raise(self) -> None: + # warns-in-raises works since https://github.com/pytest-dev/pytest/pull/11129 + with pytest.raises(ValueError, match="some exception"): + with pytest.warns(FutureWarning, match="some warning"): + warnings.warn("some warning", category=FutureWarning) + raise ValueError("some exception") + # and raises-in-warns has always worked but we'll check for symmetry. + with pytest.warns(FutureWarning, match="some warning"): + with pytest.raises(ValueError, match="some exception"): + warnings.warn("some warning", category=FutureWarning) + raise ValueError("some exception") From 7fdc8391e2cea79994e20c1ab679e960ce9d9c7c Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 16:23:18 -0700 Subject: [PATCH 17/53] Explicit GC for PyPy --- testing/test_unraisableexception.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 72f620364..08e7d6c95 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -1,11 +1,15 @@ +import sys + import pytest from _pytest.pytester import Pytester +PYPY = hasattr(sys, "pypy_version_info") + @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable(pytester: Pytester) -> None: pytester.makepyfile( - test_it=""" + test_it=f""" class BrokenDel: def __del__(self): raise ValueError("del is broken") @@ -13,6 +17,7 @@ def test_unraisable(pytester: Pytester) -> None: def test_it(): obj = BrokenDel() del obj + {"import gc; gc.collect()" * PYPY} def test_2(): pass """ @@ -37,7 +42,7 @@ def test_unraisable(pytester: Pytester) -> None: @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_setup(pytester: Pytester) -> None: pytester.makepyfile( - test_it=""" + test_it=f""" import pytest class BrokenDel: @@ -48,6 +53,7 @@ def test_unraisable_in_setup(pytester: Pytester) -> None: def broken_del(): obj = BrokenDel() del obj + {"import gc; gc.collect()" * PYPY} def test_it(broken_del): pass def test_2(): pass @@ -73,7 +79,7 @@ def test_unraisable_in_setup(pytester: Pytester) -> None: @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_teardown(pytester: Pytester) -> None: pytester.makepyfile( - test_it=""" + test_it=f""" import pytest class BrokenDel: @@ -85,6 +91,7 @@ def test_unraisable_in_teardown(pytester: Pytester) -> None: yield obj = BrokenDel() del obj + {"import gc; gc.collect()" * PYPY} def test_it(broken_del): pass def test_2(): pass @@ -110,7 +117,7 @@ def test_unraisable_in_teardown(pytester: Pytester) -> None: @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_unraisable_warning_error(pytester: Pytester) -> None: pytester.makepyfile( - test_it=""" + test_it=f""" class BrokenDel: def __del__(self) -> None: raise ValueError("del is broken") @@ -118,6 +125,7 @@ def test_unraisable_warning_error(pytester: Pytester) -> None: def test_it() -> None: obj = BrokenDel() del obj + {"import gc; gc.collect()" * PYPY} def test_2(): pass """ From 0311fc3384093b3f04e8b25166508e10e3f1d3df Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 20:18:25 -0700 Subject: [PATCH 18/53] Apply suggestions from code review Co-authored-by: Bruno Oliveira --- .github/workflows/test.yml | 3 ++- changelog/11151.breaking.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68f05a58a..b3f258f1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,7 +93,8 @@ jobs: - name: "ubuntu-py38" python: "3.8" os: ubuntu-latest - tox_env: "py38-xdist" + tox_env: "py38-lsof-numpy-pexpect" + use_coverage: true - name: "ubuntu-py38-pluggy" python: "3.8" os: ubuntu-latest diff --git a/changelog/11151.breaking.rst b/changelog/11151.breaking.rst index d846fc94c..2e86c5dfb 100644 --- a/changelog/11151.breaking.rst +++ b/changelog/11151.breaking.rst @@ -1,2 +1,2 @@ -Drop support for Python 3.7, which `reached end-of-life on 2023-06-27 +Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 `__. From 0353a94cd1ab1d961aa064235ee038d7a13583dd Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Fri, 30 Jun 2023 20:33:12 -0700 Subject: [PATCH 19/53] Explicit GC for PyPy, take 2 --- testing/test_unraisableexception.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 08e7d6c95..d255adb2b 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -6,10 +6,11 @@ from _pytest.pytester import Pytester PYPY = hasattr(sys, "pypy_version_info") +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable(pytester: Pytester) -> None: pytester.makepyfile( - test_it=f""" + test_it=""" class BrokenDel: def __del__(self): raise ValueError("del is broken") @@ -17,7 +18,6 @@ def test_unraisable(pytester: Pytester) -> None: def test_it(): obj = BrokenDel() del obj - {"import gc; gc.collect()" * PYPY} def test_2(): pass """ @@ -39,10 +39,11 @@ def test_unraisable(pytester: Pytester) -> None: ) +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_setup(pytester: Pytester) -> None: pytester.makepyfile( - test_it=f""" + test_it=""" import pytest class BrokenDel: @@ -53,7 +54,6 @@ def test_unraisable_in_setup(pytester: Pytester) -> None: def broken_del(): obj = BrokenDel() del obj - {"import gc; gc.collect()" * PYPY} def test_it(broken_del): pass def test_2(): pass @@ -76,10 +76,11 @@ def test_unraisable_in_setup(pytester: Pytester) -> None: ) +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_teardown(pytester: Pytester) -> None: pytester.makepyfile( - test_it=f""" + test_it=""" import pytest class BrokenDel: @@ -91,7 +92,6 @@ def test_unraisable_in_teardown(pytester: Pytester) -> None: yield obj = BrokenDel() del obj - {"import gc; gc.collect()" * PYPY} def test_it(broken_del): pass def test_2(): pass From 2f7415cfbc4b6ca62f9013f1abd27136f46b9653 Mon Sep 17 00:00:00 2001 From: akhilramkee <31619526+akhilramkee@users.noreply.github.com> Date: Sat, 1 Jul 2023 20:42:41 +0530 Subject: [PATCH 20/53] Add child modules as attributes of parent modules. (#10338) Failing to add child modules as attributes of parent module will prevent them from being accessible through parent module. Fix #10337 --- changelog/10337.bugfix.rst | 2 ++ src/_pytest/pathlib.py | 14 +++++++++++++- testing/test_pathlib.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 changelog/10337.bugfix.rst diff --git a/changelog/10337.bugfix.rst b/changelog/10337.bugfix.rst new file mode 100644 index 000000000..c5eeff19d --- /dev/null +++ b/changelog/10337.bugfix.rst @@ -0,0 +1,2 @@ +Fixed but that fake intermediate modules generated by ``--import-mode=importlib`` would not include the +child modules as attributes of the parent modules. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 70383e4b5..e43310ef0 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -633,6 +633,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> otherwise "src.tests.test_foo" is not importable by ``__import__``. """ module_parts = module_name.split(".") + child_module: Union[ModuleType, None] = None + module: Union[ModuleType, None] = None + child_name: str = "" while module_name: if module_name not in modules: try: @@ -642,13 +645,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> # ourselves to fall back to creating a dummy module. if not sys.meta_path: raise ModuleNotFoundError - importlib.import_module(module_name) + module = importlib.import_module(module_name) except ModuleNotFoundError: module = ModuleType( module_name, doc="Empty module created by pytest's importmode=importlib.", ) + else: + module = modules[module_name] + if child_module: + # Add child attribute to the parent that can reference the child + # modules. + if not hasattr(module, child_name): + setattr(module, child_name, child_module) modules[module_name] = module + # Keep track of the child module while moving up the tree. + child_module, child_name = module, module_name.rpartition(".")[-1] module_parts.pop(-1) module_name = ".".join(module_parts) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 56c54e484..c15a81ea1 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -592,3 +592,15 @@ class TestImportLibMode: modules = {} insert_missing_modules(modules, "") assert modules == {} + + def test_parent_contains_child_module_attribute( + self, monkeypatch: MonkeyPatch, tmp_path: Path + ): + monkeypatch.chdir(tmp_path) + # Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and + # don't end up being imported. + modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")} + insert_missing_modules(modules, "xxx.tests.foo") + assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"] + assert modules["xxx"].tests is modules["xxx.tests"] + assert modules["xxx.tests"].foo is modules["xxx.tests.foo"] From b77d0deaf53697630ba534f62dc871f4eb275f14 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Jul 2023 12:37:46 -0300 Subject: [PATCH 21/53] Fix duplicated imports with importlib mode and doctest-modules (#11148) The initial implementation (in #7246) introduced the `importlib` mode, which never added the imported module to `sys.modules`, so it included a test to ensure calling `import_path` twice would yield different modules. Not adding modules to `sys.modules` proved problematic, so we began to add the imported module to `sys.modules` in #7870, but failed to realize that given we are now changing `sys.modules`, we might as well avoid importing it more than once. Then #10088 came along, passing `importlib` also when importing application modules (as opposed to only test modules before), which caused problems due to imports having side-effects and the expectation being that they are imported only once. With this PR, `import_path` returns the module immediately if already in `sys.modules`. Fix #10811 Fix #10341 --- changelog/10811.bugfix.rst | 2 ++ src/_pytest/pathlib.py | 2 ++ testing/acceptance_test.py | 35 +++++++++++++++++++++++++++++++++++ testing/test_pathlib.py | 27 +++++++++++++++++++-------- 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 changelog/10811.bugfix.rst diff --git a/changelog/10811.bugfix.rst b/changelog/10811.bugfix.rst new file mode 100644 index 000000000..aa26414e4 --- /dev/null +++ b/changelog/10811.bugfix.rst @@ -0,0 +1,2 @@ +Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules +to be imported more than once, causing problems with modules that have import side effects. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index e43310ef0..14fb2e3ae 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -523,6 +523,8 @@ def import_path( if mode is ImportMode.importlib: module_name = module_name_from_path(path, root) + with contextlib.suppress(KeyError): + return sys.modules[module_name] for meta_importer in sys.meta_path: spec = meta_importer.find_spec(module_name, [str(path.parent)]) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 453e26972..429fb4e43 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1315,3 +1315,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None: ) res = pytester.runpytest() res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"]) + + +def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None: + """ + Regression test for #10811: previously import_path with ImportMode.importlib would + not return a module if already in sys.modules, resulting in modules being imported + multiple times, which causes problems with modules that have import side effects. + """ + # Uses the exact reproducer form #10811, given it is very minimal + # and illustrates the problem well. + pytester.makepyfile( + **{ + "pmxbot/commands.py": "from . import logging", + "pmxbot/logging.py": "", + "tests/__init__.py": "", + "tests/test_commands.py": """ + import importlib + from pmxbot import logging + + class TestCommands: + def test_boo(self): + assert importlib.import_module('pmxbot.logging') is logging + """, + } + ) + pytester.makeini( + """ + [pytest] + addopts= + --doctest-modules + --import-mode importlib + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines("*1 passed*") diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index c15a81ea1..3d574e856 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -7,6 +7,7 @@ from textwrap import dedent from types import ModuleType from typing import Any from typing import Generator +from typing import Iterator import pytest from _pytest.monkeypatch import MonkeyPatch @@ -282,29 +283,36 @@ class TestImportPath: import_path(tmp_path / "invalid.py", root=tmp_path) @pytest.fixture - def simple_module(self, tmp_path: Path) -> Path: - fn = tmp_path / "_src/tests/mymod.py" + def simple_module( + self, tmp_path: Path, request: pytest.FixtureRequest + ) -> Iterator[Path]: + name = f"mymod_{request.node.name}" + fn = tmp_path / f"_src/tests/{name}.py" fn.parent.mkdir(parents=True) fn.write_text("def foo(x): return 40 + x", encoding="utf-8") - return fn + module_name = module_name_from_path(fn, root=tmp_path) + yield fn + sys.modules.pop(module_name, None) - def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None: + def test_importmode_importlib( + self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest + ) -> None: """`importlib` mode does not change sys.path.""" module = import_path(simple_module, mode="importlib", root=tmp_path) assert module.foo(2) == 42 # type: ignore[attr-defined] assert str(simple_module.parent) not in sys.path assert module.__name__ in sys.modules - assert module.__name__ == "_src.tests.mymod" + assert module.__name__ == f"_src.tests.mymod_{request.node.name}" assert "_src" in sys.modules assert "_src.tests" in sys.modules - def test_importmode_twice_is_different_module( + def test_remembers_previous_imports( self, simple_module: Path, tmp_path: Path ) -> None: - """`importlib` mode always returns a new module.""" + """`importlib` mode called remembers previous module (#10341, #10811).""" module1 = import_path(simple_module, mode="importlib", root=tmp_path) module2 = import_path(simple_module, mode="importlib", root=tmp_path) - assert module1 is not module2 + assert module1 is module2 def test_no_meta_path_found( self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path @@ -317,6 +325,9 @@ class TestImportPath: # mode='importlib' fails if no spec is found to load the module import importlib.util + # Force module to be re-imported. + del sys.modules[module.__name__] + monkeypatch.setattr( importlib.util, "spec_from_file_location", lambda *args: None ) From 561f1a993bcef74b626a43ec16c79a57b6aa99d9 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 1 Jul 2023 22:08:21 +0200 Subject: [PATCH 22/53] fix #10447 - consider marks in reverse mro order to give base classes priority --- changelog/10447.bugfix.rst | 2 ++ src/_pytest/mark/structures.py | 4 +++- testing/test_mark.py | 37 +++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 changelog/10447.bugfix.rst diff --git a/changelog/10447.bugfix.rst b/changelog/10447.bugfix.rst new file mode 100644 index 000000000..fff94b28f --- /dev/null +++ b/changelog/10447.bugfix.rst @@ -0,0 +1,2 @@ +markers are now considered in the reverse mro order to ensure base class markers are considered first +this resolves a regression. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index bc10d3b90..d6e426567 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -374,7 +374,9 @@ def get_unpacked_marks( if not consider_mro: mark_lists = [obj.__dict__.get("pytestmark", [])] else: - mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__] + mark_lists = [ + x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__) + ] mark_list = [] for item in mark_lists: if isinstance(item, list): diff --git a/testing/test_mark.py b/testing/test_mark.py index e2d1a40c3..2767260df 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1130,6 +1130,41 @@ def test_mark_mro() -> None: all_marks = get_unpacked_marks(C) - assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark] + assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark] assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark] + + +# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447") +def test_mark_fixture_order_mro(pytester: Pytester): + """This ensures we walk marks of the mro starting with the base classes + the action at a distance fixtures are taken as minimal example from a real project + + """ + foo = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def add_attr1(request): + request.instance.attr1 = object() + + + @pytest.fixture + def add_attr2(request): + request.instance.attr2 = request.instance.attr1 + + + @pytest.mark.usefixtures('add_attr1') + class Parent: + pass + + + @pytest.mark.usefixtures('add_attr2') + class TestThings(Parent): + def test_attrs(self): + assert self.attr1 == self.attr2 + """ + ) + result = pytester.runpytest(foo) + result.assert_outcomes(passed=1) From 024e62e6d2b7c19837b567dcc972cabaa0713f16 Mon Sep 17 00:00:00 2001 From: Akhilesh Ramakrishnan <31619526+akhilramkee@users.noreply.github.com> Date: Mon, 3 Jul 2023 17:19:50 +0530 Subject: [PATCH 23/53] Added Akhilesh Ramakrishnan to AUTHORS (#11153) * Added Akhilesh Ramakrishnan to AUTHORS PR#10338 * Fix typo in changelog --- AUTHORS | 1 + changelog/10337.bugfix.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 28116c3c8..13ea94697 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Adam Johnson Adam Stewart Adam Uhlir Ahn Ki-Wook +Akhilesh Ramakrishnan Akiomi Kamakura Alan Velasco Alessio Izzo diff --git a/changelog/10337.bugfix.rst b/changelog/10337.bugfix.rst index c5eeff19d..629449e8d 100644 --- a/changelog/10337.bugfix.rst +++ b/changelog/10337.bugfix.rst @@ -1,2 +1,2 @@ -Fixed but that fake intermediate modules generated by ``--import-mode=importlib`` would not include the +Fixed that fake intermediate modules generated by ``--import-mode=importlib`` would not include the child modules as attributes of the parent modules. From b84708422421a1237e0710e0468225d017feeb30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 11:45:17 -0300 Subject: [PATCH 24/53] [automated] Update plugin list (#11159) Co-authored-by: pytest bot --- doc/en/reference/plugin_list.rst | 136 +++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 44 deletions(-) diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index f4e7570da..6c7eb6502 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -13,7 +13,7 @@ Packages classified as inactive are excluded. creating a PDF, because otherwise the table gets far too wide for the page. -This list contains 1272 plugins. +This list contains 1278 plugins. .. only:: not latex @@ -41,7 +41,7 @@ This list contains 1272 plugins. :pypi:`pytest-aioworkers` A plugin to test aioworkers project with pytest May 01, 2023 5 - Production/Stable pytest>=6.1.0 :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A - :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. May 23, 2023 N/A pytest (>=6.0) + :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Jun 27, 2023 N/A pytest (>=6.0) :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) @@ -112,7 +112,7 @@ This list contains 1272 plugins. :pypi:`pytest-base-url` pytest plugin for URL based testing Mar 27, 2022 5 - Production/Stable pytest (>=3.0.0,<8.0.0) :pypi:`pytest-bdd` BDD for pytest Nov 08, 2022 6 - Mature pytest (>=6.2.0) :pypi:`pytest-bdd-html` pytest plugin to display BDD info in HTML test report Nov 22, 2022 3 - Alpha pytest (!=6.0.0,>=5.0) - :pypi:`pytest-bdd-ng` BDD for pytest Oct 06, 2022 4 - Beta pytest (>=5.0) + :pypi:`pytest-bdd-ng` BDD for pytest Jul 01, 2023 4 - Beta pytest (>=5.0) :pypi:`pytest-bdd-splinter` Common steps for pytest bdd and splinter integration Aug 12, 2019 5 - Production/Stable pytest (>=4.0.0) :pypi:`pytest-bdd-web` A simple plugin to use with pytest Jan 02, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bdd-wrappers` Feb 11, 2020 2 - Pre-Alpha N/A @@ -229,7 +229,7 @@ This list contains 1272 plugins. :pypi:`pytest-cov` Pytest plugin for measuring coverage. May 24, 2023 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-cover` Pytest plugin for measuring coverage. Forked from \`pytest-cov\`. Aug 01, 2015 5 - Production/Stable N/A :pypi:`pytest-coverage` Jun 17, 2015 N/A N/A - :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jan 04, 2021 4 - Beta pytest (>=6.1.0) + :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jun 28, 2023 4 - Beta N/A :pypi:`pytest-coveragemarkers` Using pytest markers to track functional coverage and filtering of tests Nov 29, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-cov-exclude` Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Jan 30, 2023 5 - Production/Stable pytest (>=7.0) @@ -242,7 +242,7 @@ This list contains 1272 plugins. :pypi:`pytest-cricri` A Cricri plugin for pytest. Jan 27, 2018 N/A pytest :pypi:`pytest-crontab` add crontab task in crontab Dec 09, 2019 N/A N/A :pypi:`pytest-csv` CSV output for pytest. Apr 22, 2021 N/A pytest (>=6.0) - :pypi:`pytest-csv-params` Pytest plugin for Test Case Parametrization with CSV files Aug 28, 2022 5 - Production/Stable pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-csv-params` Pytest plugin for Test Case Parametrization with CSV files Jul 01, 2023 5 - Production/Stable pytest (>=7.4.0,<8.0.0) :pypi:`pytest-curio` Pytest support for curio. Oct 07, 2020 N/A N/A :pypi:`pytest-curl-report` pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A :pypi:`pytest-custom-concurrency` Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A @@ -383,7 +383,7 @@ This list contains 1272 plugins. :pypi:`pytest-embrace` ๐Ÿ’ Dataclasses-as-tests. Describe the runtime once and multiply coverage with no boilerplate. Mar 25, 2023 N/A pytest (>=7.0,<8.0) :pypi:`pytest-emoji` A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) :pypi:`pytest-emoji-output` Pytest plugin to represent test output with emoji support Apr 09, 2023 4 - Beta pytest (==7.0.1) - :pypi:`pytest-enabler` Enable installed pytest plugins May 12, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' + :pypi:`pytest-enabler` Enable installed pytest plugins Jun 26, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' :pypi:`pytest-encode` set your encoding and logger Nov 06, 2021 N/A N/A :pypi:`pytest-encode-kane` set your encoding and logger Nov 16, 2021 N/A pytest :pypi:`pytest-enhanced-reports` Enhanced test reports for pytest Dec 15, 2022 N/A N/A @@ -469,7 +469,7 @@ This list contains 1272 plugins. :pypi:`pytest-flask-sqlalchemy` A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions. Apr 30, 2022 4 - Beta pytest (>=3.2.1) :pypi:`pytest-flask-sqlalchemy-transactions` Run tests in transactions using pytest, Flask, and SQLalchemy. Aug 02, 2018 4 - Beta pytest (>=3.2.1) :pypi:`pytest-flexreport` Apr 15, 2023 4 - Beta pytest - :pypi:`pytest-fluent` A pytest plugin in order to provide logs via fluentd Jul 12, 2022 4 - Beta pytest + :pypi:`pytest-fluent` A pytest plugin in order to provide logs via fluentd Jun 26, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-fluentbit` A pytest plugin in order to provide logs via fluentbit Jun 16, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-flyte` Pytest fixtures for simplifying Flyte integration testing May 03, 2021 N/A pytest :pypi:`pytest-focus` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest @@ -494,7 +494,7 @@ This list contains 1272 plugins. :pypi:`pytest-gherkin` A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) :pypi:`pytest-gh-log-group` pytest plugin for gh actions Jan 11, 2022 3 - Alpha pytest :pypi:`pytest-ghostinspector` For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A - :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. Jun 14, 2023 N/A N/A + :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. Jun 28, 2023 N/A N/A :pypi:`pytest-git` Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-gitconfig` Provide a gitconfig sandbox for testing Jun 22, 2023 4 - Beta pytest>=7.1.2 :pypi:`pytest-gitcov` Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A @@ -512,6 +512,7 @@ This list contains 1272 plugins. :pypi:`pytest-google-chat` Notify google chat channel for test results Mar 27, 2022 4 - Beta pytest :pypi:`pytest-graphql-schema` Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A :pypi:`pytest-greendots` Green progress dots Feb 08, 2014 3 - Alpha N/A + :pypi:`pytest-group-by-class` A Pytest plugin for running a subset of your tests by splitting them in to groups of classes. Jun 27, 2023 5 - Production/Stable pytest (>=2.5) :pypi:`pytest-growl` Growl notifications for pytest results. Jan 13, 2014 5 - Production/Stable N/A :pypi:`pytest-grpc` pytest plugin for grpc May 01, 2020 N/A pytest (>=3.6.0) :pypi:`pytest-grunnur` Py.Test plugin for Grunnur-based packages. Feb 05, 2023 N/A N/A @@ -581,7 +582,7 @@ This list contains 1272 plugins. :pypi:`pytest-integration-mark` Automatic integration test marking and excluding plugin for pytest May 22, 2023 N/A pytest (>=5.2) :pypi:`pytest-interactive` A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A :pypi:`pytest-intercept-remote` Pytest plugin for intercepting outgoing connection requests during pytest run. May 24, 2021 4 - Beta pytest (>=4.6) - :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. May 09, 2023 4 - Beta pytest + :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. Jun 29, 2023 4 - Beta pytest :pypi:`pytest-invenio` Pytest fixtures for Invenio. Jun 02, 2023 5 - Production/Stable pytest (<7.2.0,>=6) :pypi:`pytest-involve` Run tests covering a specific file or changeset Feb 02, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-ipdb` A py.test plug-in to enable drop to ipdb debugger on test failure. Mar 20, 2013 2 - Pre-Alpha N/A @@ -680,7 +681,7 @@ This list contains 1272 plugins. :pypi:`pytest-maybe-raises` Pytest fixture for optional exception testing. May 27, 2022 N/A pytest ; extra == 'dev' :pypi:`pytest-mccabe` pytest plugin to run the mccabe code complexity checker. Jul 22, 2020 3 - Alpha pytest (>=5.4.0) :pypi:`pytest-md` Plugin for generating Markdown reports for pytest results Jul 11, 2019 3 - Alpha pytest (>=4.2.1) - :pypi:`pytest-md-report` A pytest plugin to make a test results report with Markdown table format. May 28, 2023 4 - Beta pytest (!=6.0.0,<8,>=3.3.2) + :pypi:`pytest-md-report` A pytest plugin to make a test results report with Markdown table format. Jun 25, 2023 4 - Beta pytest (!=6.0.0,<8,>=3.3.2) :pypi:`pytest-memlog` Log memory usage during tests May 03, 2023 N/A pytest (>=7.3.0,<8.0.0) :pypi:`pytest-memprof` Estimates memory consumption of test functions Mar 29, 2019 4 - Beta N/A :pypi:`pytest-memray` A simple plugin to use with pytest Jun 06, 2023 N/A pytest>=7.2 @@ -714,7 +715,7 @@ This list contains 1272 plugins. :pypi:`pytest-molecule` PyTest Molecule Plugin :: discover and run molecule tests Mar 29, 2022 5 - Production/Stable pytest (>=7.0.0) :pypi:`pytest-mongo` MongoDB process and client fixtures plugin for Pytest. Jun 07, 2021 5 - Production/Stable pytest :pypi:`pytest-mongodb` pytest plugin for MongoDB fixtures May 16, 2023 5 - Production/Stable N/A - :pypi:`pytest-monitor` Pytest plugin for analyzing resource usage. Oct 22, 2022 5 - Production/Stable pytest + :pypi:`pytest-monitor` Pytest plugin for analyzing resource usage. Jun 25, 2023 5 - Production/Stable pytest :pypi:`pytest-monkeyplus` pytest's monkeypatch subclass with extra functionalities Sep 18, 2012 5 - Production/Stable N/A :pypi:`pytest-monkeytype` pytest-monkeytype: Generate Monkeytype annotations from your pytest tests. Jul 29, 2020 4 - Beta N/A :pypi:`pytest-moto` Fixtures for integration tests of AWS services,uses moto mocking library. Aug 28, 2015 1 - Planning N/A @@ -732,7 +733,7 @@ This list contains 1272 plugins. :pypi:`pytest-mutagen` Add the mutation testing feature to pytest Jul 24, 2020 N/A pytest (>=5.4) :pypi:`pytest-mypy` Mypy static type checker plugin for Pytest Dec 18, 2022 4 - Beta pytest (>=6.2) ; python_version >= "3.10" :pypi:`pytest-mypyd` Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" - :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins May 05, 2023 4 - Beta pytest (>=6.2.0) + :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Jun 29, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-mypy-plugins-shim` Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Apr 12, 2021 N/A pytest>=6.0.0 :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Feb 25, 2023 N/A pytest>=7,<8 :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Mar 27, 2023 5 - Production/Stable pytest (>=6.2) @@ -825,6 +826,7 @@ This list contains 1272 plugins. :pypi:`pytest-play` pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A :pypi:`pytest-playbook` Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Apr 24, 2023 N/A pytest (<8.0.0,>=6.2.4) + :pypi:`pytest-playwright-async` ASYNC Pytest plugin for Playwright Jun 26, 2023 N/A N/A :pypi:`pytest-playwrights` A pytest wrapper with fixtures for Playwright to automate web browsers Dec 02, 2021 N/A N/A :pypi:`pytest-playwright-snapshot` A pytest wrapper for snapshot testing with playwright Aug 19, 2021 N/A N/A :pypi:`pytest-playwright-visual` A pytest fixture for visual testing with Playwright Apr 28, 2022 N/A N/A @@ -842,6 +844,7 @@ This list contains 1272 plugins. :pypi:`pytest-poo` Visualize your crappy tests Mar 25, 2021 5 - Production/Stable pytest (>=2.3.4) :pypi:`pytest-poo-fail` Visualize your failed tests with poo Feb 12, 2015 5 - Production/Stable N/A :pypi:`pytest-pop` A pytest plugin to help with testing pop projects May 09, 2023 5 - Production/Stable pytest + :pypi:`pytest-porringer` Jun 24, 2023 N/A pytest>=7.1.2 :pypi:`pytest-portion` Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-postgres` Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest :pypi:`pytest-postgresql` Postgresql fixtures and fixture factories for Pytest. May 20, 2023 5 - Production/Stable pytest (>=6.2) @@ -851,7 +854,7 @@ This list contains 1272 plugins. :pypi:`pytest-pretty` pytest plugin for printing summary data as I want it Apr 05, 2023 5 - Production/Stable pytest>=7 :pypi:`pytest-pretty-terminal` pytest plugin for generating prettier terminal output Jan 31, 2022 N/A pytest (>=3.4.1) :pypi:`pytest-pride` Minitest-style test colors Apr 02, 2016 3 - Alpha N/A - :pypi:`pytest-print` pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Jun 16, 2023 5 - Production/Stable pytest>=7.3.2 + :pypi:`pytest-print` pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Jun 28, 2023 5 - Production/Stable pytest>=7.3.2 :pypi:`pytest-profiles` pytest plugin for configuration profiles Dec 09, 2021 4 - Beta pytest (>=3.7.0) :pypi:`pytest-profiling` Profiling plugin for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-progress` pytest plugin for instant test progress status Jan 31, 2022 5 - Production/Stable N/A @@ -890,7 +893,7 @@ This list contains 1272 plugins. :pypi:`pytest-qaseio` Pytest plugin for Qase.io integration May 11, 2023 4 - Beta pytest (>=7.2.2,<8.0.0) :pypi:`pytest-qasync` Pytest support for qasync. Jul 12, 2021 4 - Beta pytest (>=5.4.0) :pypi:`pytest-qatouch` Pytest plugin for uploading test results to your QA Touch Testrun. Feb 14, 2023 4 - Beta pytest (>=6.2.0) - :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Jun 09, 2023 5 - Production/Stable pytest (>=6.2.5) + :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Jun 30, 2023 5 - Production/Stable pytest (>=6.2.5) :pypi:`pytest-qml` Run QML Tests with pytest Dec 02, 2020 4 - Beta pytest (>=6.0.0) :pypi:`pytest-qr` pytest plugin to generate test result QR codes Nov 25, 2021 4 - Beta N/A :pypi:`pytest-qt` pytest support for PyQt and PySide applications Oct 25, 2022 5 - Production/Stable pytest (>=3.0.0) @@ -943,7 +946,7 @@ This list contains 1272 plugins. :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility May 22, 2023 3 - Alpha pytest :pypi:`pytest-report-me` A pytest plugin to generate report. Dec 31, 2020 N/A pytest :pypi:`pytest-report-parameters` pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) - :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Jun 08, 2023 N/A pytest (>=3.8.0) + :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Jun 30, 2023 N/A pytest (>=3.8.0) :pypi:`pytest-reports` An interesting python package Jun 07, 2023 N/A N/A :pypi:`pytest-reqs` pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) :pypi:`pytest-requests` A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) @@ -1002,14 +1005,14 @@ This list contains 1272 plugins. :pypi:`pytest-sanic` a pytest plugin for Sanic Oct 25, 2021 N/A pytest (>=5.2) :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Jun 24, 2023 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Jun 30, 2023 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A :pypi:`pytest-schema` ๐Ÿ‘ Validate return values against a schema-like object in testing Mar 14, 2022 5 - Production/Stable pytest (>=3.5.0) :pypi:`pytest-securestore` An encrypted password store for use within pytest cases Nov 08, 2021 4 - Beta N/A :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) :pypi:`pytest-selenium` pytest plugin for Selenium May 28, 2023 5 - Production/Stable pytest>=6.0.0 - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Jun 24, 2023 5 - Production/Stable N/A + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Jun 30, 2023 5 - Production/Stable N/A :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A @@ -1049,6 +1052,7 @@ This list contains 1272 plugins. :pypi:`pytest-smartcov` Smart coverage plugin for pytest. Sep 30, 2017 3 - Alpha N/A :pypi:`pytest-smell` Automated bad smell detection tool for Pytest Jun 26, 2022 N/A N/A :pypi:`pytest-smtp` Send email with pytest execution result Feb 20, 2021 N/A pytest + :pypi:`pytest-smtp4dev` Plugin for smtp4dev API Jun 27, 2023 5 - Production/Stable N/A :pypi:`pytest-smtpd` An SMTP server for testing built on aiosmtpd May 15, 2023 N/A pytest :pypi:`pytest-snail` Plugin for adding a marker to slow running tests. ๐ŸŒ Nov 04, 2019 3 - Alpha pytest (>=5.0.1) :pypi:`pytest-snapci` py.test plugin for Snap-CI Nov 12, 2015 N/A N/A @@ -1076,7 +1080,7 @@ This list contains 1272 plugins. :pypi:`pytest-splitio` Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) :pypi:`pytest-split-tests` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. Jul 30, 2021 5 - Production/Stable pytest (>=2.5) :pypi:`pytest-split-tests-tresorit` Feb 22, 2021 1 - Planning N/A - :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Feb 22, 2023 N/A pytest (>5.4.0,<7.3) + :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Jun 30, 2023 N/A pytest (>5.4.0,<8) :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Mar 07, 2023 N/A N/A :pypi:`pytest-splunk-env` pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) :pypi:`pytest-sqitch` sqitch for pytest Apr 06, 2020 4 - Beta N/A @@ -1124,7 +1128,7 @@ This list contains 1272 plugins. :pypi:`pytest-tcpclient` A pytest plugin for testing TCP clients Nov 16, 2022 N/A pytest (<8,>=7.1.3) :pypi:`pytest-teamcity-logblock` py.test plugin to introduce block structure in teamcity build log, if output is not captured May 15, 2018 4 - Beta N/A :pypi:`pytest-telegram` Pytest to Telegram reporting plugin Dec 10, 2020 5 - Production/Stable N/A - :pypi:`pytest-telegram-notifier` Telegram notification plugin for Pytest Mar 17, 2023 5 - Production/Stable N/A + :pypi:`pytest-telegram-notifier` Telegram notification plugin for Pytest Jun 27, 2023 5 - Production/Stable N/A :pypi:`pytest-tempdir` Predictable and repeatable tempdir support. Oct 11, 2019 4 - Beta pytest (>=2.8.1) :pypi:`pytest-terra-fixt` Terraform and Terragrunt fixtures for pytest Sep 15, 2022 N/A pytest (==6.2.5) :pypi:`pytest-terraform` A pytest plugin for using terraform fixtures Jun 20, 2023 N/A pytest (>=6.0) @@ -1159,6 +1163,7 @@ This list contains 1272 plugins. :pypi:`pytest-test-this` Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply Sep 15, 2019 2 - Pre-Alpha pytest (>=2.3) :pypi:`pytest-test-utils` Jul 14, 2022 N/A pytest (>=5) :pypi:`pytest-tesults` Tesults plugin for pytest Dec 23, 2022 5 - Production/Stable pytest (>=3.5.0) + :pypi:`pytest-textual-snapshot` Snapshot testing for Textual apps Jun 27, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-tezos` pytest-ligo Jan 16, 2020 4 - Beta N/A :pypi:`pytest-th2-bdd` pytest_th2_bdd May 13, 2022 N/A N/A :pypi:`pytest-thawgun` Pytest plugin for time travel May 26, 2020 3 - Alpha N/A @@ -1171,6 +1176,7 @@ This list contains 1272 plugins. :pypi:`pytest-timer` A timer plugin for pytest Jun 02, 2021 N/A N/A :pypi:`pytest-timestamper` Pytest plugin to add a timestamp prefix to the pytest output Jun 06, 2021 N/A N/A :pypi:`pytest-timestamps` A simple plugin to view timestamps for each test Apr 01, 2023 N/A pytest (>=5.2) + :pypi:`pytest-tinybird` A pytest plugin to report test results to tinybird Jun 26, 2023 4 - Beta pytest (>=3.8.0) :pypi:`pytest-tipsi-django` Nov 17, 2021 4 - Beta pytest (>=6.0.0) :pypi:`pytest-tipsi-testing` Better fixtures management. Various helpers Nov 04, 2020 4 - Beta pytest (>=3.3.0) :pypi:`pytest-tldr` A pytest plugin that limits the output to just the things you need. Oct 26, 2022 4 - Beta pytest (>=3.5.0) @@ -1219,7 +1225,7 @@ This list contains 1272 plugins. :pypi:`pytest-unmarked` Run only unmarked tests Aug 27, 2019 5 - Production/Stable N/A :pypi:`pytest-unordered` Test equality of unordered collections in pytest Nov 28, 2022 4 - Beta pytest (>=6.0.0) :pypi:`pytest-unstable` Set a test as unstable to return 0 even if it failed Sep 27, 2022 4 - Beta N/A - :pypi:`pytest-unused-fixtures` A pytest plugin to list unused fixtures after a test run. Jun 15, 2023 4 - Beta pytest (>=7.3.2,<8.0.0) + :pypi:`pytest-unused-fixtures` A pytest plugin to list unused fixtures after a test run. Jun 30, 2023 4 - Beta pytest (>=7.3.2,<8.0.0) :pypi:`pytest-upload-report` pytest-upload-report is a plugin for pytest that upload your test report for test results. Jun 18, 2021 5 - Production/Stable N/A :pypi:`pytest-utils` Some helpers for pytest. Feb 02, 2023 4 - Beta pytest (>=7.0.0,<8.0.0) :pypi:`pytest-vagrant` A py.test plugin providing access to vagrant. Sep 07, 2021 5 - Production/Stable pytest @@ -1445,7 +1451,7 @@ This list contains 1272 plugins. :pypi:`pytest-alembic` - *last release*: May 23, 2023, + *last release*: Jun 27, 2023, *status*: N/A, *requires*: pytest (>=6.0) @@ -1942,7 +1948,7 @@ This list contains 1272 plugins. pytest plugin to display BDD info in HTML test report :pypi:`pytest-bdd-ng` - *last release*: Oct 06, 2022, + *last release*: Jul 01, 2023, *status*: 4 - Beta, *requires*: pytest (>=5.0) @@ -2761,9 +2767,9 @@ This list contains 1272 plugins. :pypi:`pytest-coverage-context` - *last release*: Jan 04, 2021, + *last release*: Jun 28, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.1.0) + *requires*: N/A Coverage dynamic context support for PyTest, including sub-processes @@ -2852,9 +2858,9 @@ This list contains 1272 plugins. CSV output for pytest. :pypi:`pytest-csv-params` - *last release*: Aug 28, 2022, + *last release*: Jul 01, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=7.1.2,<8.0.0) + *requires*: pytest (>=7.4.0,<8.0.0) Pytest plugin for Test Case Parametrization with CSV files @@ -3839,7 +3845,7 @@ This list contains 1272 plugins. Pytest plugin to represent test output with emoji support :pypi:`pytest-enabler` - *last release*: May 12, 2023, + *last release*: Jun 26, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=6) ; extra == 'testing' @@ -4441,9 +4447,9 @@ This list contains 1272 plugins. :pypi:`pytest-fluent` - *last release*: Jul 12, 2022, + *last release*: Jun 26, 2023, *status*: 4 - Beta, - *requires*: pytest + *requires*: pytest (>=7.0.0) A pytest plugin in order to provide logs via fluentd @@ -4616,7 +4622,7 @@ This list contains 1272 plugins. For finding/executing Ghost Inspector tests :pypi:`pytest-girder` - *last release*: Jun 14, 2023, + *last release*: Jun 28, 2023, *status*: N/A, *requires*: N/A @@ -4741,6 +4747,13 @@ This list contains 1272 plugins. Green progress dots + :pypi:`pytest-group-by-class` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=2.5) + + A Pytest plugin for running a subset of your tests by splitting them in to groups of classes. + :pypi:`pytest-growl` *last release*: Jan 13, 2014, *status*: 5 - Production/Stable, @@ -5225,7 +5238,7 @@ This list contains 1272 plugins. Pytest plugin for intercepting outgoing connection requests during pytest run. :pypi:`pytest-interface-tester` - *last release*: May 09, 2023, + *last release*: Jun 29, 2023, *status*: 4 - Beta, *requires*: pytest @@ -5918,7 +5931,7 @@ This list contains 1272 plugins. Plugin for generating Markdown reports for pytest results :pypi:`pytest-md-report` - *last release*: May 28, 2023, + *last release*: Jun 25, 2023, *status*: 4 - Beta, *requires*: pytest (!=6.0.0,<8,>=3.3.2) @@ -6156,7 +6169,7 @@ This list contains 1272 plugins. pytest plugin for MongoDB fixtures :pypi:`pytest-monitor` - *last release*: Oct 22, 2022, + *last release*: Jun 25, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -6282,9 +6295,9 @@ This list contains 1272 plugins. Mypy static type checker plugin for Pytest :pypi:`pytest-mypy-plugins` - *last release*: May 05, 2023, + *last release*: Jun 29, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.2.0) + *requires*: pytest (>=7.0.0) pytest plugin for writing tests for mypy plugins @@ -6932,6 +6945,13 @@ This list contains 1272 plugins. A pytest wrapper with fixtures for Playwright to automate web browsers + :pypi:`pytest-playwright-async` + *last release*: Jun 26, 2023, + *status*: N/A, + *requires*: N/A + + ASYNC Pytest plugin for Playwright + :pypi:`pytest-playwrights` *last release*: Dec 02, 2021, *status*: N/A, @@ -7051,6 +7071,13 @@ This list contains 1272 plugins. A pytest plugin to help with testing pop projects + :pypi:`pytest-porringer` + *last release*: Jun 24, 2023, + *status*: N/A, + *requires*: pytest>=7.1.2 + + + :pypi:`pytest-portion` *last release*: Jan 28, 2021, *status*: 4 - Beta, @@ -7115,7 +7142,7 @@ This list contains 1272 plugins. Minitest-style test colors :pypi:`pytest-print` - *last release*: Jun 16, 2023, + *last release*: Jun 28, 2023, *status*: 5 - Production/Stable, *requires*: pytest>=7.3.2 @@ -7388,7 +7415,7 @@ This list contains 1272 plugins. Pytest plugin for uploading test results to your QA Touch Testrun. :pypi:`pytest-qgis` - *last release*: Jun 09, 2023, + *last release*: Jun 30, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=6.2.5) @@ -7759,7 +7786,7 @@ This list contains 1272 plugins. pytest plugin for adding tests' parameters to junit report :pypi:`pytest-reportportal` - *last release*: Jun 08, 2023, + *last release*: Jun 30, 2023, *status*: N/A, *requires*: pytest (>=3.8.0) @@ -8172,7 +8199,7 @@ This list contains 1272 plugins. :pypi:`pytest-sbase` - *last release*: Jun 24, 2023, + *last release*: Jun 30, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8221,7 +8248,7 @@ This list contains 1272 plugins. pytest plugin for Selenium :pypi:`pytest-seleniumbase` - *last release*: Jun 24, 2023, + *last release*: Jun 30, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8500,6 +8527,13 @@ This list contains 1272 plugins. Send email with pytest execution result + :pypi:`pytest-smtp4dev` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Plugin for smtp4dev API + :pypi:`pytest-smtpd` *last release*: May 15, 2023, *status*: N/A, @@ -8690,9 +8724,9 @@ This list contains 1272 plugins. :pypi:`pytest-splunk-addon` - *last release*: Feb 22, 2023, + *last release*: Jun 30, 2023, *status*: N/A, - *requires*: pytest (>5.4.0,<7.3) + *requires*: pytest (>5.4.0,<8) A Dynamic test tool for Splunk Apps and Add-ons @@ -9026,7 +9060,7 @@ This list contains 1272 plugins. Pytest to Telegram reporting plugin :pypi:`pytest-telegram-notifier` - *last release*: Mar 17, 2023, + *last release*: Jun 27, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -9270,6 +9304,13 @@ This list contains 1272 plugins. Tesults plugin for pytest + :pypi:`pytest-textual-snapshot` + *last release*: Jun 27, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + Snapshot testing for Textual apps + :pypi:`pytest-tezos` *last release*: Jan 16, 2020, *status*: 4 - Beta, @@ -9354,6 +9395,13 @@ This list contains 1272 plugins. A simple plugin to view timestamps for each test + :pypi:`pytest-tinybird` + *last release*: Jun 26, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.8.0) + + A pytest plugin to report test results to tinybird + :pypi:`pytest-tipsi-django` *last release*: Nov 17, 2021, *status*: 4 - Beta, @@ -9691,7 +9739,7 @@ This list contains 1272 plugins. Set a test as unstable to return 0 even if it failed :pypi:`pytest-unused-fixtures` - *last release*: Jun 15, 2023, + *last release*: Jun 30, 2023, *status*: 4 - Beta, *requires*: pytest (>=7.3.2,<8.0.0) From bea56b30af3dd7d682993b72d870c8ac7c864460 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Mon, 3 Jul 2023 17:52:58 +0200 Subject: [PATCH 25/53] Switch to deploy environment and configure for pypi oidc (#10925) Closes #10871 Closes #10870 --- .github/workflows/deploy.yml | 40 ++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 186adc55a..166d14a54 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,40 +13,54 @@ on: permissions: {} jobs: - - deploy: - if: github.repository == 'pytest-dev/pytest' - + build: runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write - + timeout-minutes: 10 steps: - uses: actions/checkout@v3 with: fetch-depth: 0 persist-credentials: false - - name: Build and Check Package uses: hynek/build-and-inspect-python-package@v1.5 + deploy: + if: github.repository == 'pytest-dev/pytest' + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + id-token: write + steps: - name: Download Package uses: actions/download-artifact@v3 with: name: Packages path: dist - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.pypi_token }} + uses: pypa/gh-action-pypi-publish@v1.8.5 + release-notes: + + # todo: generate the content in the build job + # the goal being of using a github action script to push the release data + # after success instead of creating a complete python/tox env + needs: [deploy] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + persist-credentials: false - name: Set up Python uses: actions/setup-python@v4 with: python-version: "3.11" + - name: Install tox run: | python -m pip install --upgrade pip From 6995257cf470d2143ad1683824962de4071c0eb7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Jul 2023 07:00:14 +0000 Subject: [PATCH 26/53] [pre-commit.ci] pre-commit autoupdate (#11165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.7.0 โ†’ v3.8.0](https://github.com/asottile/pyupgrade/compare/v3.7.0...v3.8.0) - [github.com/asottile/setup-cfg-fmt: v2.3.0 โ†’ v2.4.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.3.0...v2.4.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94ec67408..33816f0d0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,12 +42,12 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src', --py38-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 + rev: v3.8.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.3.0 + rev: v2.4.0 hooks: - id: setup-cfg-fmt args: ["--max-py-version=3.12", "--include-version-classifiers"] From 18e87c98319faee46f2becd32cf6518c9efaceaa Mon Sep 17 00:00:00 2001 From: Lesnek Date: Sun, 2 Jul 2023 14:15:50 +0200 Subject: [PATCH 27/53] test(warnings-recorder): Add non working subclass behaviour of pop --- testing/test_recwarn.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 16b8d5443..b15ef2cc1 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -37,6 +37,26 @@ def test_recwarn_captures_deprecation_warning(recwarn: WarningsRecorder) -> None assert recwarn.pop(DeprecationWarning) +class TestSubclassWarningPop: + class ParentWarning(Warning): + pass + + class ChildWarning(ParentWarning): + pass + + def raise_warnings(self): + warnings.warn("Warning Child", self.ChildWarning) + warnings.warn("Warning Parent", self.ParentWarning) + + def test_pop(self): + with pytest.warns((self.ParentWarning, self.ChildWarning)) as record: + self.raise_warnings() + + assert len(record) == 2 + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ParentWarning + + class TestWarningsRecorderChecker: def test_recording(self) -> None: rec = WarningsRecorder(_ispytest=True) From 8ac3c645fa4a87afb30295d28e8d6fb67166bd01 Mon Sep 17 00:00:00 2001 From: Lesnek Date: Sun, 2 Jul 2023 14:17:06 +0200 Subject: [PATCH 28/53] fix(warnings-recorder): Match also subclass of warning in pop --- src/_pytest/recwarn.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index ff8e70820..ba5a3311c 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -206,12 +206,18 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] return len(self._list) def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning, raise exception if not exists.""" + """Pop the first recorded warning (or subclass of warning), raise exception if not exists.""" + matches = [] for i, w in enumerate(self._list): - if issubclass(w.category, cls): + if w.category == cls: return self._list.pop(i) - __tracebackhide__ = True - raise AssertionError(f"{cls!r} not found in warning list") + if issubclass(w.category, cls): + matches.append((i, w)) + if not matches: + __tracebackhide__ = True + raise AssertionError(f"{cls!r} not found in warning list") + (idx, best), *rest = matches + return self._list.pop(idx) def clear(self) -> None: """Clear the list of recorded warnings.""" From 2706271f66ea1b8d3e931e6e945bb4dbbd758fea Mon Sep 17 00:00:00 2001 From: Lesnek Date: Sun, 2 Jul 2023 15:03:15 +0200 Subject: [PATCH 29/53] test(warnings-recorder): Add another warning --- testing/test_recwarn.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index b15ef2cc1..72dc34b09 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -44,7 +44,11 @@ class TestSubclassWarningPop: class ChildWarning(ParentWarning): pass + class RandomWarning(Warning): + pass + def raise_warnings(self): + warnings.warn("Warning Random", self.RandomWarning) warnings.warn("Warning Child", self.ChildWarning) warnings.warn("Warning Parent", self.ParentWarning) From 7b7bd304aafcf9287eafc73bb332e4081f8fdf58 Mon Sep 17 00:00:00 2001 From: Lesnek Date: Sun, 2 Jul 2023 15:16:14 +0200 Subject: [PATCH 30/53] fix(warnings-recorder): Add handling of rest --- src/_pytest/recwarn.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index ba5a3311c..a60f34671 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -217,6 +217,11 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] __tracebackhide__ = True raise AssertionError(f"{cls!r} not found in warning list") (idx, best), *rest = matches + for i, w in rest: + if issubclass(w.category, best.category) and not issubclass( + best.category, w.category + ): + idx, best = i, w return self._list.pop(idx) def clear(self) -> None: From 3d0dedb5ec7ac56c5f3e8bdbf5b47d7276096151 Mon Sep 17 00:00:00 2001 From: Lesnek Date: Sun, 2 Jul 2023 15:35:04 +0200 Subject: [PATCH 31/53] test(warnings-recorder): Add attribute error test --- testing/test_recwarn.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 72dc34b09..6c2dc4660 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -1,5 +1,7 @@ import warnings +from typing import List from typing import Optional +from typing import Type import pytest from _pytest.pytester import Pytester @@ -44,22 +46,37 @@ class TestSubclassWarningPop: class ChildWarning(ParentWarning): pass - class RandomWarning(Warning): + class ChildOfChildWarning(ChildWarning): pass - def raise_warnings(self): - warnings.warn("Warning Random", self.RandomWarning) - warnings.warn("Warning Child", self.ChildWarning) - warnings.warn("Warning Parent", self.ParentWarning) + @staticmethod + def raise_warnings_from_list(_warnings: List[Type[Warning]]): + for warn in _warnings: + warnings.warn(f"Warning {warn().__repr__()}", warn) def test_pop(self): with pytest.warns((self.ParentWarning, self.ChildWarning)) as record: - self.raise_warnings() + self.raise_warnings_from_list( + [self.ChildWarning, self.ParentWarning, self.ChildOfChildWarning] + ) - assert len(record) == 2 + assert len(record) == 3 _warn = record.pop(self.ParentWarning) assert _warn.category is self.ParentWarning + def test_pop_raises(self): + with pytest.raises(AssertionError): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list([self.ParentWarning]) + record.pop(self.ChildOfChildWarning) + + def test_pop_most_recent(self): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list([self.ChildWarning, self.ChildOfChildWarning]) + + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ChildOfChildWarning + class TestWarningsRecorderChecker: def test_recording(self) -> None: From 4517af1e28083068cfe6d5d441f151dfd27a98b4 Mon Sep 17 00:00:00 2001 From: Lesnek Date: Sun, 2 Jul 2023 15:35:04 +0200 Subject: [PATCH 32/53] test(warnings-recorder): Add attribute error test --- AUTHORS | 1 + changelog/10701.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/10701.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 13ea94697..062f565c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -263,6 +263,7 @@ Mickey Pashov Mihai Capotฤƒ Mike Hoyle (hoylemd) Mike Lundy +Milan Lesnek Miro Hronฤok Nathaniel Compton Nathaniel Waisbrot diff --git a/changelog/10701.bugfix.rst b/changelog/10701.bugfix.rst new file mode 100644 index 000000000..d02ddacab --- /dev/null +++ b/changelog/10701.bugfix.rst @@ -0,0 +1 @@ +``pytest.WarningsRecorder.pop`` now also pop most recent subclass of warning. From 6badb6f01e355633eda251d037b48e39a4ed89dc Mon Sep 17 00:00:00 2001 From: Milan Lesnek Date: Tue, 4 Jul 2023 08:59:58 +0200 Subject: [PATCH 33/53] Apply suggestions from code review chore(changelog): describe better the fix Co-authored-by: Zac Hatfield-Dodds --- changelog/10701.bugfix.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog/10701.bugfix.rst b/changelog/10701.bugfix.rst index d02ddacab..f33fa7fb2 100644 --- a/changelog/10701.bugfix.rst +++ b/changelog/10701.bugfix.rst @@ -1 +1,2 @@ -``pytest.WarningsRecorder.pop`` now also pop most recent subclass of warning. +:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list, +rather than the first warning which is an instance of the requested type. From c4876c710628b8bd244ae255aef529c369f8dd63 Mon Sep 17 00:00:00 2001 From: Lesnek Date: Tue, 4 Jul 2023 10:20:50 +0200 Subject: [PATCH 34/53] chore(CR): Add changes from code review --- pyproject.toml | 2 +- src/_pytest/recwarn.py | 31 +++++++++++++++++-------------- testing/test_recwarn.py | 6 ++++-- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a4139a5c0..8432d5d2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" write_to = "src/_pytest/_version.py" [tool.pytest.ini_options] -minversion = "2.0" +#minversion = "2.0" addopts = "-rfEX -p pytester --strict-markers" python_files = ["test_*.py", "*_test.py", "testing/python/*.py"] python_classes = ["Test", "Acceptance"] diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index a60f34671..9d8816556 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -206,23 +206,26 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] return len(self._list) def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning (or subclass of warning), raise exception if not exists.""" - matches = [] + """Pop the first recorded warning which is an instance of ``cls``. + + But not an instance of a child class of any other match. + Raises ``AssertionError`` if there is no match. + + """ + + best_idx = None for i, w in enumerate(self._list): if w.category == cls: - return self._list.pop(i) - if issubclass(w.category, cls): - matches.append((i, w)) - if not matches: - __tracebackhide__ = True - raise AssertionError(f"{cls!r} not found in warning list") - (idx, best), *rest = matches - for i, w in rest: - if issubclass(w.category, best.category) and not issubclass( - best.category, w.category + return self._list.pop(i) # exact match, stop looking + if issubclass(w.category, cls) and ( + best_idx is None + or not issubclass(w.category, self._list[best_idx].category) # type: ignore[unreachable] ): - idx, best = i, w - return self._list.pop(idx) + best_idx = i + if best_idx is not None: + return self._list.pop(best_idx) + __tracebackhide__ = True + raise AssertionError(f"{cls!r} not found in warning list") def clear(self) -> None: """Clear the list of recorded warnings.""" diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 6c2dc4660..3a35c255e 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -72,10 +72,12 @@ class TestSubclassWarningPop: def test_pop_most_recent(self): with pytest.warns(self.ParentWarning) as record: - self.raise_warnings_from_list([self.ChildWarning, self.ChildOfChildWarning]) + self.raise_warnings_from_list( + [self.ChildOfChildWarning, self.ChildWarning, self.ChildOfChildWarning] + ) _warn = record.pop(self.ParentWarning) - assert _warn.category is self.ChildOfChildWarning + assert _warn.category is self.ChildWarning class TestWarningsRecorderChecker: From 6baf9f2d31ce0a7f65a52efbd42038b407a79e9c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 4 Jul 2023 17:20:51 +0300 Subject: [PATCH 35/53] Update docs and code after dropping EOL 3.7 --- doc/en/index.rst | 2 +- pyproject.toml | 2 +- testing/_py/test_local.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 23b6964c9..90b720ea5 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -18,7 +18,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries. -``pytest`` requires: Python 3.7+ or PyPy3. +``pytest`` requires: Python 3.8+ or PyPy3. **PyPI package name**: :pypi:`pytest` diff --git a/pyproject.toml b/pyproject.toml index a4139a5c0..d540773c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ template = "changelog/_template.rst" showcontent = true [tool.black] -target-version = ['py37'] +target-version = ['py38'] # check-wheel-contents is executed by the build-and-inspect-python-package action. [tool.check-wheel-contents] diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index 348682b53..895066a9f 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -1519,9 +1519,9 @@ class TestPOSIXLocalPath: path1.chown(owner, group) -class TestUnicodePy2Py3: +class TestUnicode: def test_join_ensure(self, tmpdir, monkeypatch): - if sys.version_info >= (3, 0) and "LANG" not in os.environ: + if "LANG" not in os.environ: pytest.skip("cannot run test without locale") x = local(tmpdir.strpath) part = "hรคllo" @@ -1529,7 +1529,7 @@ class TestUnicodePy2Py3: assert x.join(part) == y def test_listdir(self, tmpdir): - if sys.version_info >= (3, 0) and "LANG" not in os.environ: + if "LANG" not in os.environ: pytest.skip("cannot run test without locale") x = local(tmpdir.strpath) part = "hรคllo" From 1f5058e9722ed59a857d3d568e2560ae0ec96059 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 4 Jul 2023 17:22:05 +0300 Subject: [PATCH 36/53] Use same Black version for blacken-docs as regular Black check --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33816f0d0..d85abe263 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: rev: 1.14.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.1.0] + additional_dependencies: [black==23.3.0] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: From 7775e494b155176b2c7083bce1e25b236401b638 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 4 Jul 2023 10:00:29 -0700 Subject: [PATCH 37/53] Further tweaks from code review --- pyproject.toml | 2 +- src/_pytest/recwarn.py | 11 ++++------- testing/test_recwarn.py | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8432d5d2b..a4139a5c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" write_to = "src/_pytest/_version.py" [tool.pytest.ini_options] -#minversion = "2.0" +minversion = "2.0" addopts = "-rfEX -p pytester --strict-markers" python_files = ["test_*.py", "*_test.py", "testing/python/*.py"] python_classes = ["Test", "Acceptance"] diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 9d8816556..5484d6f3b 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -206,20 +206,17 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] return len(self._list) def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning which is an instance of ``cls``. - - But not an instance of a child class of any other match. + """Pop the first recorded warning which is an instance of ``cls``, + but not an instance of a child class of any other match. Raises ``AssertionError`` if there is no match. - """ - - best_idx = None + best_idx: Optional[int] = None for i, w in enumerate(self._list): if w.category == cls: return self._list.pop(i) # exact match, stop looking if issubclass(w.category, cls) and ( best_idx is None - or not issubclass(w.category, self._list[best_idx].category) # type: ignore[unreachable] + or not issubclass(w.category, self._list[best_idx].category) ): best_idx = i if best_idx is not None: diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 3a35c255e..8b70c8aff 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -54,7 +54,7 @@ class TestSubclassWarningPop: for warn in _warnings: warnings.warn(f"Warning {warn().__repr__()}", warn) - def test_pop(self): + def test_pop_finds_exact_match(self): with pytest.warns((self.ParentWarning, self.ChildWarning)) as record: self.raise_warnings_from_list( [self.ChildWarning, self.ParentWarning, self.ChildOfChildWarning] @@ -64,13 +64,13 @@ class TestSubclassWarningPop: _warn = record.pop(self.ParentWarning) assert _warn.category is self.ParentWarning - def test_pop_raises(self): + def test_pop_raises_if_no_match(self): with pytest.raises(AssertionError): with pytest.warns(self.ParentWarning) as record: self.raise_warnings_from_list([self.ParentWarning]) record.pop(self.ChildOfChildWarning) - def test_pop_most_recent(self): + def test_pop_finds_best_inexact_match(self): with pytest.warns(self.ParentWarning) as record: self.raise_warnings_from_list( [self.ChildOfChildWarning, self.ChildWarning, self.ChildOfChildWarning] From b5bc53e441bd52fdb1948739eb0420db3ad6a7ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Jul 2023 23:19:24 +0200 Subject: [PATCH 38/53] Update open trainings (#11171) --- doc/en/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 23b6964c9..a2745e98d 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,8 +2,9 @@ .. sidebar:: Next Open Trainings - - `pytest tips and tricks for a better testsuite `_, at `Europython 2023 `_, July 18th (3h), Prague/Remote - - `Professional Testing with Python `_, via `Python Academy `_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote + - `pytest tips and tricks for a better testsuite `_, at `Europython 2023 `_, **July 18th** (3h), **Prague, Czech Republic / Remote** + - `pytest: Professionelles Testen (nicht nur) fรผr Python `_, at `Workshoptage 2023 `_, **September 5th**, `OST `_ Campus **Rapperswil, Switzerland** + - `Professional Testing with Python `_, via `Python Academy `_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote** Also see :doc:`previous talks and blogposts `. From 119cec02792766e01fe6ad725526679e9bdc69a4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 5 Jul 2023 18:57:40 +0300 Subject: [PATCH 39/53] tox: turn off PYTHONWARNDEFAULTENCODING for pre-commit, sphinx etc. Fix #11157. --- tox.ini | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tox.ini b/tox.ini index 9cdbfe3ab..1b62b98e4 100644 --- a/tox.ini +++ b/tox.ini @@ -74,6 +74,9 @@ skip_install = True basepython = python3 deps = pre-commit>=2.9.3 commands = pre-commit run --all-files --show-diff-on-failure {posargs:} +setenv = + # pre-commit and tools it launches are not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:docs] basepython = python3 @@ -88,6 +91,9 @@ commands = # changelog in the docs; this does not happen on ReadTheDocs because it uses # the standard sphinx command so the 'changelog_towncrier_draft' is never set there sphinx-build -W --keep-going -b html doc/en doc/en/_build/html -t changelog_towncrier_draft {posargs:} +setenv = + # Sphinx is not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:docs-checklinks] basepython = python3 @@ -96,6 +102,9 @@ changedir = doc/en deps = -r{toxinidir}/doc/en/requirements.txt commands = sphinx-build -W -q --keep-going -b linkcheck . _build +setenv = + # Sphinx is not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:regen] changedir = doc/en @@ -110,6 +119,9 @@ allowlist_externals = make commands = make regen +setenv = + # We don't want this warning to reach regen output. + PYTHONWARNDEFAULTENCODING= [testenv:plugins] # use latest versions of all plugins, including pre-releases From 44604f49cdd370682b08e6820a70aab93ce80f8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Jul 2023 22:57:45 +0000 Subject: [PATCH 40/53] build(deps): Bump django in /testing/plugins_integration Bumps [django](https://github.com/django/django) from 4.2.2 to 4.2.3. - [Commits](https://github.com/django/django/compare/4.2.2...4.2.3) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index d56720c02..efdf837ce 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,5 +1,5 @@ anyio[curio,trio]==3.7.0 -django==4.2.2 +django==4.2.3 pytest-asyncio==0.21.0 pytest-bdd==6.1.1 pytest-cov==4.1.0 From ddd773ecb1d7631df86de9a09327c7eb003dd627 Mon Sep 17 00:00:00 2001 From: mickeypash Date: Wed, 31 Mar 2021 11:54:33 +0100 Subject: [PATCH 41/53] [docs] add table nose x pytest naming comparison --- doc/en/how-to/nose.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/doc/en/how-to/nose.rst b/doc/en/how-to/nose.rst index a736dfa55..9ef56c6d7 100644 --- a/doc/en/how-to/nose.rst +++ b/doc/en/how-to/nose.rst @@ -70,6 +70,34 @@ Unsupported idioms / known issues fundamentally incompatible with pytest because they don't support fixtures properly since collection and test execution are separated. +Here is a table comparing the default supported naming conventions for both +nose and pytest. + +========================== ======== ====== +Convention nose pytest +========================== ======== ====== +โ“‚ test*.py โœ… +โ“‚ test_*.py โœ… โœ… +โ“‚ \*_test.py โœ… +โ“‚ \*_tests.py +โ’ธ \*\(unittest.TestCase\) โœ… โœ… +โ“œ test_\* โœ… โœ… +โ’ธ Test\* โœ… +โ“œ test_\* โœ… +โ“• test_\* โœ… +========================== ======== ====== + +Symbols are described below + +======= ========= +Legend +======= ========= +โ“‚ module +โ’ธ Class +โ“œ method +โ“• function +======= ========= + Migrating from nose to pytest ------------------------------ From b81003f6fbc4fe540f4e6c2c5ef13c4d52a49964 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Thu, 6 Jul 2023 00:04:15 -0700 Subject: [PATCH 42/53] Incorporate legend into main table --- doc/en/how-to/nose.rst | 43 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/doc/en/how-to/nose.rst b/doc/en/how-to/nose.rst index 9ef56c6d7..45d3357cf 100644 --- a/doc/en/how-to/nose.rst +++ b/doc/en/how-to/nose.rst @@ -47,8 +47,7 @@ Unsupported idioms / known issues - nose imports test modules with the same import path (e.g. ``tests.test_mode``) but different file system paths (e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``) - by extending sys.path/import semantics. pytest does not do that - but there is discussion in :issue:`268` for adding some support. Note that + by extending sys.path/import semantics. pytest does not do that. Note that `nose2 choose to avoid this sys.path/import hackery `_. If you place a conftest.py file in the root directory of your project @@ -66,44 +65,34 @@ Unsupported idioms / known issues - no nose-configuration is recognized. -- ``yield``-based methods are unsupported as of pytest 4.1.0. They are +- ``yield``-based methods are fundamentally incompatible with pytest because they don't support fixtures properly since collection and test execution are separated. Here is a table comparing the default supported naming conventions for both nose and pytest. -========================== ======== ====== -Convention nose pytest -========================== ======== ====== -โ“‚ test*.py โœ… -โ“‚ test_*.py โœ… โœ… -โ“‚ \*_test.py โœ… -โ“‚ \*_tests.py -โ’ธ \*\(unittest.TestCase\) โœ… โœ… -โ“œ test_\* โœ… โœ… -โ’ธ Test\* โœ… -โ“œ test_\* โœ… -โ“• test_\* โœ… -========================== ======== ====== +========= ========================== ======= ===== +what default naming convention pytest nose +========= ========================== ======= ===== +module ``test*.py`` โœ… +module ``test_*.py`` โœ… โœ… +module ``*_test.py`` โœ… +module ``*_tests.py`` +class ``*(unittest.TestCase)`` โœ… โœ… +method ``test_*`` โœ… โœ… +class ``Test*`` โœ… +method ``test_*`` โœ… +function ``test_*`` โœ… +========= ========================== ======= ===== -Symbols are described below - -======= ========= -Legend -======= ========= -โ“‚ module -โ’ธ Class -โ“œ method -โ“• function -======= ========= Migrating from nose to pytest ------------------------------ `nose2pytest `_ is a Python script and pytest plugin to help convert Nose-based tests into pytest-based tests. -Specifically, the script transforms nose.tools.assert_* function calls into +Specifically, the script transforms ``nose.tools.assert_*`` function calls into raw assert statements, while preserving format of original arguments as much as possible. From c5b13099e6e121e6e9991855136371dbad53fbad Mon Sep 17 00:00:00 2001 From: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> Date: Fri, 7 Jul 2023 14:42:59 -0400 Subject: [PATCH 43/53] Fix error assertion handling in approx when None in dict comparison Dict comparsion in the ApproxMapping class did not check if values were None before attempting to subtract for max_abs_diff stat, which was throwing an TypeError instead of being handled by pytest error assertion. Check for None has been added before these calculations, so that None will properly show as Obtained/Expected in pytest assert message --- changelog/10702.bugfix.rst | 1 + src/_pytest/python_api.py | 25 +++++++++++++------------ testing/python/approx.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 changelog/10702.bugfix.rst diff --git a/changelog/10702.bugfix.rst b/changelog/10702.bugfix.rst new file mode 100644 index 000000000..4008cc882 --- /dev/null +++ b/changelog/10702.bugfix.rst @@ -0,0 +1 @@ +Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 0967ae8ec..35560f168 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -265,19 +265,20 @@ class ApproxMapping(ApproxBase): approx_side_as_map.items(), other_side.values() ): if approx_value != other_value: - max_abs_diff = max( - max_abs_diff, abs(approx_value.expected - other_value) - ) - if approx_value.expected == 0.0: - max_rel_diff = math.inf - else: - max_rel_diff = max( - max_rel_diff, - abs( - (approx_value.expected - other_value) - / approx_value.expected - ), + if not any((approx_value.expected is None, other_value is None)): + max_abs_diff = max( + max_abs_diff, abs(approx_value.expected - other_value) ) + if approx_value.expected == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max( + max_rel_diff, + abs( + (approx_value.expected - other_value) + / approx_value.expected + ), + ) different_ids.append(approx_key) message_data = [ diff --git a/testing/python/approx.py b/testing/python/approx.py index 631e52b56..6ad411a3e 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -122,6 +122,23 @@ class TestApprox: ], ) + assert_approx_raises_regex( + {"a": 1.0, "b": None, "c": None}, + { + "a": None, + "b": 1000.0, + "c": None, + }, + [ + r" comparison failed. Mismatched elements: 2 / 3:", + r" Max absolute difference: -inf", + r" Max relative difference: -inf", + r" Index \| Obtained\s+\| Expected\s+", + rf" a \| {SOME_FLOAT} \| None", + rf" b \| None\s+\| {SOME_FLOAT} ยฑ {SOME_FLOAT}", + ], + ) + assert_approx_raises_regex( [1.0, 2.0, 3.0, 4.0], [1.0, 3.0, 3.0, 5.0], From d4265448a58434f624c8d1817a7173a168b0f300 Mon Sep 17 00:00:00 2001 From: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> Date: Sat, 8 Jul 2023 08:41:42 -0400 Subject: [PATCH 44/53] Update src/_pytest/python_api.py Co-authored-by: Bruno Oliveira --- src/_pytest/python_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 35560f168..200b2b3aa 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -265,7 +265,7 @@ class ApproxMapping(ApproxBase): approx_side_as_map.items(), other_side.values() ): if approx_value != other_value: - if not any((approx_value.expected is None, other_value is None)): + if approx_value.expected is not None and other_value is not None: max_abs_diff = max( max_abs_diff, abs(approx_value.expected - other_value) ) From b73ec8e5d117a244b696ff4f9d2dfb74df078b2f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 8 Jul 2023 21:40:05 +0300 Subject: [PATCH 45/53] doc: fix EncodingWarnings in examples (#11158) Otherwise the warnings show in the regen output. --- doc/en/example/nonpython/conftest.py | 2 +- doc/en/example/simple.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- doc/en/how-to/tmp_path.rst | 4 ++-- doc/en/how-to/unittest.rst | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index dd1ebe88d..e969e3e25 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -12,7 +12,7 @@ class YamlFile(pytest.File): # We need a yaml parser, e.g. PyYAML. import yaml - raw = yaml.safe_load(self.path.open()) + raw = yaml.safe_load(self.path.open(encoding="utf-8")) for name, spec in sorted(raw.items()): yield YamlItem.from_parent(self, name=name, spec=spec) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 97a6dd9f4..32e5188b7 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -817,7 +817,7 @@ case we just write some information out to a ``failures`` file: # we only look at actual failing test calls, not setup/teardown if rep.when == "call" and rep.failed: mode = "a" if os.path.exists("failures") else "w" - with open("failures", mode) as f: + with open("failures", mode, encoding="utf-8") as f: # let's also access a fixture for the fun of it if "tmp_path" in item.fixturenames: extra = " ({})".format(item.funcargs["tmp_path"]) diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index c6047af12..b2fb24b3f 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker: class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] - with open("myfile", "w") as f: + with open("myfile", "w", encoding="utf-8") as f: f.write("hello") def test_cwd_again_starts_empty(self): diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index 792933dd8..d5573f584 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -24,8 +24,8 @@ created in the `base temporary directory`_. d = tmp_path / "sub" d.mkdir() p = d / "hello.txt" - p.write_text(CONTENT) - assert p.read_text() == CONTENT + p.write_text(CONTENT, encoding="utf-8") + assert p.read_text(encoding="utf-8") == CONTENT assert len(list(tmp_path.iterdir())) == 1 assert 0 diff --git a/doc/en/how-to/unittest.rst b/doc/en/how-to/unittest.rst index 37caf6e9f..7856c1a49 100644 --- a/doc/en/how-to/unittest.rst +++ b/doc/en/how-to/unittest.rst @@ -207,10 +207,10 @@ creation of a per-test temporary directory: @pytest.fixture(autouse=True) def initdir(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory - tmp_path.joinpath("samplefile.ini").write_text("# testdata") + tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8") def test_method(self): - with open("samplefile.ini") as f: + with open("samplefile.ini", encoding="utf-8") as f: s = f.read() assert "testdata" in s From a3b4220d762ac2afd72577a7cbd4a0a006f89830 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 9 Jul 2023 00:29:15 +0000 Subject: [PATCH 46/53] [automated] Update plugin list --- doc/en/reference/plugin_list.rst | 144 +++++++++++++++++++------------ 1 file changed, 88 insertions(+), 56 deletions(-) diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index 6c7eb6502..f15bc4be5 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -13,7 +13,7 @@ Packages classified as inactive are excluded. creating a PDF, because otherwise the table gets far too wide for the page. -This list contains 1278 plugins. +This list contains 1282 plugins. .. only:: not latex @@ -41,7 +41,7 @@ This list contains 1278 plugins. :pypi:`pytest-aioworkers` A plugin to test aioworkers project with pytest May 01, 2023 5 - Production/Stable pytest>=6.1.0 :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A - :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Jun 27, 2023 N/A pytest (>=6.0) + :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Jul 06, 2023 N/A pytest (>=6.0) :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) @@ -85,6 +85,7 @@ This list contains 1278 plugins. :pypi:`pytest-astropy` Meta-package containing dependencies for testing Apr 12, 2022 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-astropy-header` pytest plugin to add diagnostic information to the header of the test output Sep 06, 2022 3 - Alpha pytest (>=4.6) :pypi:`pytest-ast-transformer` May 04, 2019 3 - Alpha pytest + :pypi:`pytest-async-generators` Pytest fixtures for async generators Jul 05, 2023 N/A N/A :pypi:`pytest-asyncio` Pytest support for asyncio Mar 19, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. May 31, 2023 N/A N/A :pypi:`pytest-asyncio-network-simulator` pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) @@ -118,7 +119,7 @@ This list contains 1278 plugins. :pypi:`pytest-bdd-wrappers` Feb 11, 2020 2 - Pre-Alpha N/A :pypi:`pytest-beakerlib` A pytest plugin that reports test results to the BeakerLib framework Mar 17, 2017 5 - Production/Stable pytest :pypi:`pytest-beds` Fixtures for testing Google Appengine (GAE) apps Jun 07, 2016 4 - Beta N/A - :pypi:`pytest-beeprint` use icdiff for better error messages in pytest assertions Jun 09, 2023 4 - Beta N/A + :pypi:`pytest-beeprint` use icdiff for better error messages in pytest assertions Jul 04, 2023 4 - Beta N/A :pypi:`pytest-bench` Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A :pypi:`pytest-benchmark` A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. Oct 25, 2022 5 - Production/Stable pytest (>=3.8) :pypi:`pytest-better-datadir` A small example package Mar 13, 2023 N/A N/A @@ -206,7 +207,7 @@ This list contains 1278 plugins. :pypi:`pytest-codegen` Automatically create pytest test signatures Aug 23, 2020 2 - Pre-Alpha N/A :pypi:`pytest-codeowners` Pytest plugin for selecting tests by GitHub CODEOWNERS. Mar 30, 2022 4 - Beta pytest (>=6.0.0) :pypi:`pytest-codestyle` pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A - :pypi:`pytest-codspeed` Pytest plugin to create CodSpeed benchmarks Dec 02, 2022 5 - Production/Stable pytest>=3.8 + :pypi:`pytest-codspeed` Pytest plugin to create CodSpeed benchmarks Jul 04, 2023 5 - Production/Stable pytest>=3.8 :pypi:`pytest-collect-formatter` Formatter for pytest collect output Mar 29, 2021 5 - Production/Stable N/A :pypi:`pytest-collect-formatter2` Formatter for pytest collect output May 31, 2021 5 - Production/Stable N/A :pypi:`pytest-collector` Python package for collecting pytest. Aug 02, 2022 N/A pytest (>=7.0,<8.0) @@ -346,6 +347,7 @@ This list contains 1278 plugins. :pypi:`pytest-doctest-ellipsis-markers` Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A :pypi:`pytest-doctest-import` A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Jun 08, 2023 3 - Alpha pytest (>=4.6) + :pypi:`pytest-dogu-report` pytest plugin for dogu report Jul 07, 2023 N/A N/A :pypi:`pytest-dolphin` Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) :pypi:`pytest-doorstop` A pytest plugin for adding test results into doorstop items. Jun 09, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-dotenv` A py.test plugin that parses environment files before running tests Jun 16, 2020 4 - Beta pytest (>=5.0.0) @@ -373,13 +375,13 @@ This list contains 1278 plugins. :pypi:`pytest-eliot` An eliot plugin for pytest. Aug 31, 2022 1 - Planning pytest (>=5.4.0) :pypi:`pytest-elk-reporter` A simple plugin to use with pytest Jan 24, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-email` Send execution result email Jul 08, 2020 N/A pytest - :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Jun 14, 2023 5 - Production/Stable pytest>=7.0 - :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Jun 14, 2023 5 - Production/Stable N/A - :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Jun 14, 2023 5 - Production/Stable N/A - :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Jun 14, 2023 5 - Production/Stable N/A - :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Jun 14, 2023 5 - Production/Stable N/A - :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Jun 14, 2023 5 - Production/Stable N/A - :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Jun 14, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Jul 05, 2023 5 - Production/Stable pytest>=7.0 + :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Jul 05, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Jul 05, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Jul 05, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Jul 05, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Jul 05, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Jul 05, 2023 5 - Production/Stable N/A :pypi:`pytest-embrace` ๐Ÿ’ Dataclasses-as-tests. Describe the runtime once and multiply coverage with no boilerplate. Mar 25, 2023 N/A pytest (>=7.0,<8.0) :pypi:`pytest-emoji` A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) :pypi:`pytest-emoji-output` Pytest plugin to represent test output with emoji support Apr 09, 2023 4 - Beta pytest (==7.0.1) @@ -528,7 +530,7 @@ This list contains 1278 plugins. :pypi:`pytest-historic` Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest :pypi:`pytest-historic-hook` Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A - :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Jun 24, 2023 3 - Alpha pytest (==7.3.1) + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Jul 07, 2023 3 - Alpha pytest (==7.3.1) :pypi:`pytest-honey` A simple plugin to use with pytest Jan 07, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A :pypi:`pytest-hot-reloading` Jun 23, 2023 N/A N/A @@ -571,7 +573,7 @@ This list contains 1278 plugins. :pypi:`pytest-ini` Reuse pytest.ini to store env variables Apr 26, 2022 N/A N/A :pypi:`pytest-inline` A pytest plugin for writing inline tests. Feb 08, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-inmanta` A py.test plugin providing fixtures to simplify inmanta modules testing. Feb 23, 2023 5 - Production/Stable N/A - :pypi:`pytest-inmanta-extensions` Inmanta tests package Apr 12, 2023 5 - Production/Stable N/A + :pypi:`pytest-inmanta-extensions` Inmanta tests package Jul 04, 2023 5 - Production/Stable N/A :pypi:`pytest-inmanta-lsm` Common fixtures for inmanta LSM related modules May 17, 2023 5 - Production/Stable N/A :pypi:`pytest-inmanta-yang` Common fixtures used in inmanta yang related modules Jun 16, 2022 4 - Beta N/A :pypi:`pytest-Inomaly` A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A @@ -739,7 +741,7 @@ This list contains 1278 plugins. :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Mar 27, 2023 5 - Production/Stable pytest (>=6.2) :pypi:`pytest-needle` pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) :pypi:`pytest-neo` pytest-neo is a plugin for pytest that shows tests like screen of Matrix. Jan 08, 2022 3 - Alpha pytest (>=6.2.0) - :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Jun 19, 2023 N/A pytest (>=3.5.0) + :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Jul 06, 2023 N/A pytest (>=3.5.0) :pypi:`pytest-network` A simple plugin to disable network on socket level. May 07, 2020 N/A N/A :pypi:`pytest-network-endpoints` Network endpoints plugin for pytest Mar 06, 2022 N/A pytest :pypi:`pytest-never-sleep` pytest plugin helps to avoid adding tests without mock \`time.sleep\` May 05, 2021 3 - Alpha pytest (>=3.5.1) @@ -763,7 +765,7 @@ This list contains 1278 plugins. :pypi:`pytest-oar` PyTest plugin for the OAR testing framework May 02, 2023 N/A pytest>=6.0.1 :pypi:`pytest-object-getter` Import any object from a 3rd party module while mocking its namespace on demand. Jul 31, 2022 5 - Production/Stable pytest :pypi:`pytest-ochrus` pytest results data-base and HTML reporter Feb 21, 2018 4 - Beta N/A - :pypi:`pytest-odoo` py.test plugin to run Odoo tests Nov 17, 2022 4 - Beta pytest (>=7.2.0) + :pypi:`pytest-odoo` py.test plugin to run Odoo tests Jul 06, 2023 4 - Beta pytest (>=7.2.0) :pypi:`pytest-odoo-fixtures` Project description Jun 25, 2019 N/A N/A :pypi:`pytest-oerp` pytest plugin to test OpenERP modules Feb 28, 2012 3 - Alpha N/A :pypi:`pytest-offline` Mar 09, 2023 1 - Planning pytest (>=7.0.0,<8.0.0) @@ -809,7 +811,7 @@ This list contains 1278 plugins. :pypi:`pytest-percent` Change the exit code of pytest test sessions when a required percent of tests pass. May 21, 2020 N/A pytest (>=5.2.0) :pypi:`pytest-perf` Run performance tests against the mainline code. Jun 02, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' :pypi:`pytest-performance` A simple plugin to ensure the execution of critical sections of code has not been impacted Sep 11, 2020 5 - Production/Stable pytest (>=3.7.0) - :pypi:`pytest-persistence` Pytest tool for persistent objects Jun 14, 2023 N/A N/A + :pypi:`pytest-persistence` Pytest tool for persistent objects Jul 04, 2023 N/A N/A :pypi:`pytest-pg` A tiny plugin for pytest which runs PostgreSQL in Docker May 04, 2023 5 - Production/Stable pytest (>=6.0.0) :pypi:`pytest-pgsql` Pytest plugins and helpers for tests using a Postgres database. May 13, 2020 5 - Production/Stable pytest (>=3.0.0) :pypi:`pytest-phmdoctest` pytest plugin to test Python examples in Markdown using phmdoctest. Apr 15, 2022 4 - Beta pytest (>=5.4.3) @@ -826,7 +828,7 @@ This list contains 1278 plugins. :pypi:`pytest-play` pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A :pypi:`pytest-playbook` Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Apr 24, 2023 N/A pytest (<8.0.0,>=6.2.4) - :pypi:`pytest-playwright-async` ASYNC Pytest plugin for Playwright Jun 26, 2023 N/A N/A + :pypi:`pytest-playwright-async` ASYNC Pytest plugin for Playwright Jul 03, 2023 N/A N/A :pypi:`pytest-playwrights` A pytest wrapper with fixtures for Playwright to automate web browsers Dec 02, 2021 N/A N/A :pypi:`pytest-playwright-snapshot` A pytest wrapper for snapshot testing with playwright Aug 19, 2021 N/A N/A :pypi:`pytest-playwright-visual` A pytest fixture for visual testing with Playwright Apr 28, 2022 N/A N/A @@ -882,7 +884,7 @@ This list contains 1278 plugins. :pypi:`pytest-pyq` Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A :pypi:`pytest-pyramid` pytest_pyramid - provides fixtures for testing pyramid applications with pytest test suite Dec 13, 2022 5 - Production/Stable pytest :pypi:`pytest-pyramid-server` Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-pyreport` PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report May 08, 2023 N/A pytest (>=7.3.1) + :pypi:`pytest-pyreport` PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report Jul 02, 2023 N/A pytest (>=7.3.1) :pypi:`pytest-pyright` Pytest plugin for type checking code with Pyright Nov 20, 2022 4 - Beta pytest (>=7.0.0) :pypi:`pytest-pyspec` A plugin that transforms the pytest output into a result similar to the RSpec. It enables the use of docstrings to display results and also enables the use of the prefixes "describe", "with" and "it". Mar 12, 2023 5 - Production/Stable pytest (>=7.2.1,<8.0.0) :pypi:`pytest-pystack` Plugin to run pystack after a timeout for a test suite. May 07, 2023 N/A pytest (>=3.5.0) @@ -900,7 +902,7 @@ This list contains 1278 plugins. :pypi:`pytest-qt-app` QT app fixture for py.test Dec 23, 2015 5 - Production/Stable N/A :pypi:`pytest-quarantine` A plugin for pytest to manage expected test failures Nov 24, 2019 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-quickcheck` pytest plugin to generate random data inspired by QuickCheck Nov 05, 2022 4 - Beta pytest (>=4.0) - :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Jun 16, 2023 5 - Production/Stable pytest (>=6.2) + :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Jul 05, 2023 5 - Production/Stable pytest (>=6.2) :pypi:`pytest-race` Race conditions tester for pytest Jun 07, 2022 4 - Beta N/A :pypi:`pytest-rage` pytest plugin to implement PEP712 Oct 21, 2011 3 - Alpha N/A :pypi:`pytest-rail` pytest plugin for creating TestRail runs and adding results May 02, 2022 N/A pytest (>=3.6) @@ -932,7 +934,7 @@ This list contains 1278 plugins. :pypi:`pytest-remfiles` Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A :pypi:`pytest-remotedata` Pytest plugin for controlling remote data access. Dec 12, 2022 3 - Alpha pytest (>=4.6) :pypi:`pytest-remote-response` Pytest plugin for capturing and mocking connection requests. Apr 26, 2023 5 - Production/Stable pytest (>=4.6) - :pypi:`pytest-remove-stale-bytecode` py.test plugin to remove stale byte code files. Mar 04, 2020 4 - Beta pytest + :pypi:`pytest-remove-stale-bytecode` py.test plugin to remove stale byte code files. Jul 07, 2023 4 - Beta pytest :pypi:`pytest-reorder` Reorder tests depending on their paths and names. May 31, 2018 4 - Beta pytest :pypi:`pytest-repeat` pytest plugin for repeating tests Oct 31, 2020 5 - Production/Stable pytest (>=3.6) :pypi:`pytest-replay` Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests Jun 09, 2021 4 - Beta pytest (>=3.0.0) @@ -955,7 +957,7 @@ This list contains 1278 plugins. :pypi:`pytest-requires` A pytest plugin to elegantly skip tests with optional requirements Dec 21, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-reraise` Make multi-threaded pytest test cases fail when they should Sep 20, 2022 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-rerun` Re-run only changed files in specified branch Jul 08, 2019 N/A pytest (>=3.6) - :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Mar 09, 2023 5 - Production/Stable pytest (>=5.3) + :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Jul 05, 2023 5 - Production/Stable pytest (>=6.2) :pypi:`pytest-rerunfailures-all-logs` pytest plugin to re-run tests to eliminate flaky failures Mar 07, 2022 5 - Production/Stable N/A :pypi:`pytest-reserial` Pytest fixture for recording and replaying serial port traffic. Apr 26, 2023 4 - Beta pytest :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest Jun 01, 2023 N/A pytest (~=4.6) ; python_version == "2.7" @@ -1005,14 +1007,14 @@ This list contains 1278 plugins. :pypi:`pytest-sanic` a pytest plugin for Sanic Oct 25, 2021 N/A pytest (>=5.2) :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Jun 30, 2023 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Jul 08, 2023 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A :pypi:`pytest-schema` ๐Ÿ‘ Validate return values against a schema-like object in testing Mar 14, 2022 5 - Production/Stable pytest (>=3.5.0) :pypi:`pytest-securestore` An encrypted password store for use within pytest cases Nov 08, 2021 4 - Beta N/A :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) :pypi:`pytest-selenium` pytest plugin for Selenium May 28, 2023 5 - Production/Stable pytest>=6.0.0 - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Jun 30, 2023 5 - Production/Stable N/A + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Jul 08, 2023 5 - Production/Stable N/A :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A @@ -1031,7 +1033,7 @@ This list contains 1278 plugins. :pypi:`pytest-share-hdf` Plugin to save test data in HDF files and retrieve them for comparison Sep 21, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-sharkreport` this is pytest report plugin. Jul 11, 2022 N/A pytest (>=3.5) :pypi:`pytest-shell` A pytest plugin to help with testing shell scripts / black box commands Mar 27, 2022 N/A N/A - :pypi:`pytest-shell-utilities` Pytest plugin to simplify running shell commands against the system Sep 23, 2022 4 - Beta pytest (>=6.0.0) + :pypi:`pytest-shell-utilities` Pytest plugin to simplify running shell commands against the system Jul 02, 2023 5 - Production/Stable pytest (>=7.1.0) :pypi:`pytest-sheraf` Versatile ZODB abstraction layer - pytest fixtures Feb 11, 2020 N/A pytest :pypi:`pytest-sherlock` pytest plugin help to find coupled tests Jan 16, 2023 5 - Production/Stable pytest (>=3.5.1) :pypi:`pytest-shortcuts` Expand command-line shortcuts listed in pytest configuration Oct 29, 2020 4 - Beta pytest (>=3.5.0) @@ -1113,6 +1115,7 @@ This list contains 1278 plugins. :pypi:`pytest-subunit` pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Aug 29, 2017 N/A N/A :pypi:`pytest-sugar` pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Apr 10, 2023 4 - Beta pytest (>=6.2.0) :pypi:`pytest-suitemanager` A simple plugin to use with pytest Apr 28, 2023 4 - Beta N/A + :pypi:`pytest-supercov` Pytest plugin for measuring explicit test-file to source-file coverage Jul 02, 2023 N/A N/A :pypi:`pytest-svn` SVN repository fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-symbols` pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. Nov 20, 2017 3 - Alpha N/A :pypi:`pytest-system-statistics` Pytest plugin to track and report system usage statistics Feb 16, 2022 5 - Production/Stable pytest (>=6.0.0) @@ -1167,6 +1170,7 @@ This list contains 1278 plugins. :pypi:`pytest-tezos` pytest-ligo Jan 16, 2020 4 - Beta N/A :pypi:`pytest-th2-bdd` pytest_th2_bdd May 13, 2022 N/A N/A :pypi:`pytest-thawgun` Pytest plugin for time travel May 26, 2020 3 - Alpha N/A + :pypi:`pytest-thread` Jul 07, 2023 N/A N/A :pypi:`pytest-threadleak` Detects thread leaks Jul 03, 2022 4 - Beta pytest (>=3.1.1) :pypi:`pytest-tick` Ticking on tests Aug 31, 2021 5 - Production/Stable pytest (>=6.2.5,<7.0.0) :pypi:`pytest-time` Jun 24, 2023 3 - Alpha pytest @@ -1273,7 +1277,7 @@ This list contains 1278 plugins. :pypi:`pytest-xfaillist` Maintain a xfaillist in an additional file to avoid merge-conflicts. Sep 17, 2021 N/A pytest (>=6.2.2,<7.0.0) :pypi:`pytest-xfiles` Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A :pypi:`pytest-xlog` Extended logging for test and decorators May 31, 2020 4 - Beta N/A - :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Mar 01, 2023 N/A pytest>=7.2.0 + :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Jul 03, 2023 N/A pytest<8,>=7.4.0 :pypi:`pytest-xpara` An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest :pypi:`pytest-xprocess` A pytest plugin for managing processes across test runs. Jan 05, 2023 4 - Beta pytest (>=2.8) :pypi:`pytest-xray` May 30, 2019 3 - Alpha N/A @@ -1283,7 +1287,7 @@ This list contains 1278 plugins. :pypi:`pytest-xvfb` A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests. May 29, 2023 4 - Beta pytest (>=2.8.1) :pypi:`pytest-xvirt` A pytest plugin to virtualize test. For example to transparently running them on a remote box. Jun 18, 2023 4 - Beta pytest (>=7.1.0) :pypi:`pytest-yaml` This plugin is used to load yaml output to your test using pytest framework. Oct 05, 2018 N/A pytest - :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml May 28, 2023 N/A pytest>=7.2.0 + :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml Jul 03, 2023 N/A pytest>=7.4.0 :pypi:`pytest-yamltree` Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) :pypi:`pytest-yamlwsgi` Run tests against wsgi apps defined in yaml May 11, 2010 N/A N/A :pypi:`pytest-yaml-yoyo` http/https API run by yaml Jun 19, 2023 N/A pytest (>=7.2.0) @@ -1451,7 +1455,7 @@ This list contains 1278 plugins. :pypi:`pytest-alembic` - *last release*: Jun 27, 2023, + *last release*: Jul 06, 2023, *status*: N/A, *requires*: pytest (>=6.0) @@ -1758,6 +1762,13 @@ This list contains 1278 plugins. + :pypi:`pytest-async-generators` + *last release*: Jul 05, 2023, + *status*: N/A, + *requires*: N/A + + Pytest fixtures for async generators + :pypi:`pytest-asyncio` *last release*: Mar 19, 2023, *status*: 4 - Beta, @@ -1990,7 +2001,7 @@ This list contains 1278 plugins. Fixtures for testing Google Appengine (GAE) apps :pypi:`pytest-beeprint` - *last release*: Jun 09, 2023, + *last release*: Jul 04, 2023, *status*: 4 - Beta, *requires*: N/A @@ -2606,7 +2617,7 @@ This list contains 1278 plugins. pytest plugin to run pycodestyle :pypi:`pytest-codspeed` - *last release*: Dec 02, 2022, + *last release*: Jul 04, 2023, *status*: 5 - Production/Stable, *requires*: pytest>=3.8 @@ -3585,6 +3596,13 @@ This list contains 1278 plugins. Pytest plugin with advanced doctest features. + :pypi:`pytest-dogu-report` + *last release*: Jul 07, 2023, + *status*: N/A, + *requires*: N/A + + pytest plugin for dogu report + :pypi:`pytest-dolphin` *last release*: Nov 30, 2016, *status*: 4 - Beta, @@ -3775,49 +3793,49 @@ This list contains 1278 plugins. Send execution result email :pypi:`pytest-embedded` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: pytest>=7.0 A pytest plugin that designed for embedded testing. :pypi:`pytest-embedded-arduino` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Arduino. :pypi:`pytest-embedded-idf` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with ESP-IDF. :pypi:`pytest-embedded-jtag` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with JTAG. :pypi:`pytest-embedded-qemu` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with QEMU. :pypi:`pytest-embedded-serial` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Serial. :pypi:`pytest-embedded-serial-esp` - *last release*: Jun 14, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -4860,7 +4878,7 @@ This list contains 1278 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: Jun 24, 2023, + *last release*: Jul 07, 2023, *status*: 3 - Alpha, *requires*: pytest (==7.3.1) @@ -5161,7 +5179,7 @@ This list contains 1278 plugins. A py.test plugin providing fixtures to simplify inmanta modules testing. :pypi:`pytest-inmanta-extensions` - *last release*: Apr 12, 2023, + *last release*: Jul 04, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -6337,7 +6355,7 @@ This list contains 1278 plugins. pytest-neo is a plugin for pytest that shows tests like screen of Matrix. :pypi:`pytest-netdut` - *last release*: Jun 19, 2023, + *last release*: Jul 06, 2023, *status*: N/A, *requires*: pytest (>=3.5.0) @@ -6505,7 +6523,7 @@ This list contains 1278 plugins. pytest results data-base and HTML reporter :pypi:`pytest-odoo` - *last release*: Nov 17, 2022, + *last release*: Jul 06, 2023, *status*: 4 - Beta, *requires*: pytest (>=7.2.0) @@ -6827,7 +6845,7 @@ This list contains 1278 plugins. A simple plugin to ensure the execution of critical sections of code has not been impacted :pypi:`pytest-persistence` - *last release*: Jun 14, 2023, + *last release*: Jul 04, 2023, *status*: N/A, *requires*: N/A @@ -6946,7 +6964,7 @@ This list contains 1278 plugins. A pytest wrapper with fixtures for Playwright to automate web browsers :pypi:`pytest-playwright-async` - *last release*: Jun 26, 2023, + *last release*: Jul 03, 2023, *status*: N/A, *requires*: N/A @@ -7338,7 +7356,7 @@ This list contains 1278 plugins. Pyramid server fixture for py.test :pypi:`pytest-pyreport` - *last release*: May 08, 2023, + *last release*: Jul 02, 2023, *status*: N/A, *requires*: pytest (>=7.3.1) @@ -7464,7 +7482,7 @@ This list contains 1278 plugins. pytest plugin to generate random data inspired by QuickCheck :pypi:`pytest-rabbitmq` - *last release*: Jun 16, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=6.2) @@ -7688,7 +7706,7 @@ This list contains 1278 plugins. Pytest plugin for capturing and mocking connection requests. :pypi:`pytest-remove-stale-bytecode` - *last release*: Mar 04, 2020, + *last release*: Jul 07, 2023, *status*: 4 - Beta, *requires*: pytest @@ -7849,9 +7867,9 @@ This list contains 1278 plugins. Re-run only changed files in specified branch :pypi:`pytest-rerunfailures` - *last release*: Mar 09, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.3) + *requires*: pytest (>=6.2) pytest plugin to re-run tests to eliminate flaky failures @@ -8199,7 +8217,7 @@ This list contains 1278 plugins. :pypi:`pytest-sbase` - *last release*: Jun 30, 2023, + *last release*: Jul 08, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8248,7 +8266,7 @@ This list contains 1278 plugins. pytest plugin for Selenium :pypi:`pytest-seleniumbase` - *last release*: Jun 30, 2023, + *last release*: Jul 08, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8381,9 +8399,9 @@ This list contains 1278 plugins. A pytest plugin to help with testing shell scripts / black box commands :pypi:`pytest-shell-utilities` - *last release*: Sep 23, 2022, - *status*: 4 - Beta, - *requires*: pytest (>=6.0.0) + *last release*: Jul 02, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.1.0) Pytest plugin to simplify running shell commands against the system @@ -8954,6 +8972,13 @@ This list contains 1278 plugins. A simple plugin to use with pytest + :pypi:`pytest-supercov` + *last release*: Jul 02, 2023, + *status*: N/A, + *requires*: N/A + + Pytest plugin for measuring explicit test-file to source-file coverage + :pypi:`pytest-svn` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -9332,6 +9357,13 @@ This list contains 1278 plugins. Pytest plugin for time travel + :pypi:`pytest-thread` + *last release*: Jul 07, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-threadleak` *last release*: Jul 03, 2022, *status*: 4 - Beta, @@ -10075,9 +10107,9 @@ This list contains 1278 plugins. Extended logging for test and decorators :pypi:`pytest-xlsx` - *last release*: Mar 01, 2023, + *last release*: Jul 03, 2023, *status*: N/A, - *requires*: pytest>=7.2.0 + *requires*: pytest<8,>=7.4.0 pytest plugin for generating test cases by xlsx(excel) @@ -10145,9 +10177,9 @@ This list contains 1278 plugins. This plugin is used to load yaml output to your test using pytest framework. :pypi:`pytest-yaml-sanmu` - *last release*: May 28, 2023, + *last release*: Jul 03, 2023, *status*: N/A, - *requires*: pytest>=7.2.0 + *requires*: pytest>=7.4.0 pytest plugin for generating test cases by yaml From 99ab8ae884a3772adc119e7fba22253b1a82eede Mon Sep 17 00:00:00 2001 From: antosikv <79337398+antosikv@users.noreply.github.com> Date: Sun, 9 Jul 2023 17:30:33 +0200 Subject: [PATCH 47/53] Clarify docs for pytest.main default behavior (#11187) Add explicit mentions of the default behavior to both API reference and how-to section about ways to invoke pytest. Co-authored-by: Ran Benita --- doc/en/how-to/usage.rst | 3 ++- src/_pytest/config/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/en/how-to/usage.rst b/doc/en/how-to/usage.rst index 8e2dd8673..4ec4459a2 100644 --- a/doc/en/how-to/usage.rst +++ b/doc/en/how-to/usage.rst @@ -173,7 +173,8 @@ You can invoke ``pytest`` from Python code directly: this acts as if you would call "pytest" from the command line. It will not raise :class:`SystemExit` but return the :ref:`exit code ` instead. -You can pass in options and arguments: +If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable. +You can pass in options and arguments explicitly: .. code-block:: python diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 47651ad9f..3e38594a8 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -137,7 +137,9 @@ def main( ) -> Union[int, ExitCode]: """Perform an in-process test run. - :param args: List of command line arguments. + :param args: + List of command line arguments. If `None` or not given, defaults to reading + arguments directly from the process command line (:data:`sys.argv`). :param plugins: List of plugin objects to be auto-registered during initialization. :returns: An exit code. From 4e75bff71a948ffc2e67b1decb00de39a87f1995 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:23:12 -0300 Subject: [PATCH 48/53] build(deps): Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.7 (#11189) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.5 to 1.8.7. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.5...v1.8.7) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 166d14a54..32feb92d5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,7 +38,7 @@ jobs: name: Packages path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.5 + uses: pypa/gh-action-pypi-publish@v1.8.7 release-notes: From b91d5a311289afc5d67ffec80ab1755e6fe6d3a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:23:33 -0300 Subject: [PATCH 49/53] build(deps): Bump pytest-rerunfailures in /testing/plugins_integration (#11190) Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 11.1.2 to 12.0. - [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/11.1.2...12.0) --- updated-dependencies: - dependency-name: pytest-rerunfailures dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index efdf837ce..92a64b452 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -7,7 +7,7 @@ pytest-django==4.5.2 pytest-flakes==4.0.5 pytest-html==3.2.0 pytest-mock==3.11.1 -pytest-rerunfailures==11.1.2 +pytest-rerunfailures==12.0 pytest-sugar==0.9.7 pytest-trio==0.7.0 pytest-twisted==1.14.0 From b20e7f6d0cd85f439d7963b4ae7c3c3fc908452d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:23:47 -0300 Subject: [PATCH 50/53] build(deps): Bump anyio[curio,trio] in /testing/plugins_integration (#11191) Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 3.7.0 to 3.7.1. - [Changelog](https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst) - [Commits](https://github.com/agronholm/anyio/compare/3.7.0...3.7.1) --- updated-dependencies: - dependency-name: anyio[curio,trio] dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- testing/plugins_integration/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index 92a64b452..c99c9f8dc 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,4 +1,4 @@ -anyio[curio,trio]==3.7.0 +anyio[curio,trio]==3.7.1 django==4.2.3 pytest-asyncio==0.21.0 pytest-bdd==6.1.1 From f1c9570a0e3f480c82658cf2a8df4107020c841d Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 2 Jul 2023 00:19:04 +0300 Subject: [PATCH 51/53] A few more Python>=3.8 simplifications --- doc/en/how-to/failures.rst | 4 ---- src/_pytest/_code/code.py | 10 +++++----- src/_pytest/pathlib.py | 18 ------------------ src/_pytest/pytester.py | 3 +-- 4 files changed, 6 insertions(+), 29 deletions(-) diff --git a/doc/en/how-to/failures.rst b/doc/en/how-to/failures.rst index ef8755091..b3d0c155b 100644 --- a/doc/en/how-to/failures.rst +++ b/doc/en/how-to/failures.rst @@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions .. versionadded:: 6.2 -.. note:: - - These features only work on Python>=3.8. - Unhandled exceptions are exceptions that are raised in a situation in which they cannot propagate to a caller. The most common case is an exception raised in a :meth:`__del__ ` implementation. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 58483d2a0..42c5fa8bd 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -393,11 +393,11 @@ class Traceback(List[TracebackEntry]): def filter( self, - # TODO(py38): change to positional only. - _excinfo_or_fn: Union[ + excinfo_or_fn: Union[ "ExceptionInfo[BaseException]", Callable[[TracebackEntry], bool], ], + /, ) -> "Traceback": """Return a Traceback instance with certain items removed. @@ -408,10 +408,10 @@ class Traceback(List[TracebackEntry]): ``TracebackEntry`` instance, and should return True when the item should be added to the ``Traceback``, False when not. """ - if isinstance(_excinfo_or_fn, ExceptionInfo): - fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731 + if isinstance(excinfo_or_fn, ExceptionInfo): + fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731 else: - fn = _excinfo_or_fn + fn = excinfo_or_fn return Traceback(filter(fn, self)) def recursionindex(self) -> Optional[int]: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 14fb2e3ae..138a0bdb2 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -769,21 +769,3 @@ def bestrelpath(directory: Path, dest: Path) -> str: # Forward from base to dest. *reldest.parts, ) - - -# Originates from py. path.local.copy(), with siginficant trims and adjustments. -# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True) -def copytree(source: Path, target: Path) -> None: - """Recursively copy a source directory to target.""" - assert source.is_dir() - for entry in visit(source, recurse=lambda entry: not entry.is_symlink()): - x = Path(entry) - relpath = x.relative_to(source) - newx = target / relpath - newx.parent.mkdir(exist_ok=True) - if x.is_symlink(): - newx.symlink_to(os.readlink(x)) - elif x.is_file(): - shutil.copyfile(x, newx) - elif x.is_dir(): - newx.mkdir(exist_ok=True) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0129c224f..d8dd3b9de 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -63,7 +63,6 @@ from _pytest.outcomes import fail from _pytest.outcomes import importorskip from _pytest.outcomes import skip from _pytest.pathlib import bestrelpath -from _pytest.pathlib import copytree from _pytest.pathlib import make_numbered_dir from _pytest.reports import CollectReport from _pytest.reports import TestReport @@ -971,7 +970,7 @@ class Pytester: example_path = example_dir.joinpath(name) if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): - copytree(example_path, self.path) + shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True) return self.path elif example_path.is_file(): result = self.path.joinpath(example_path.name) From 7008385253939271c7046f37f4008e1b7dcbe75c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:37:07 +0000 Subject: [PATCH 52/53] [pre-commit.ci] pre-commit autoupdate (#11195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 23.3.0 โ†’ 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) - [github.com/asottile/blacken-docs: 1.14.0 โ†’ 1.15.0](https://github.com/asottile/blacken-docs/compare/1.14.0...1.15.0) - [github.com/asottile/pyupgrade: v3.8.0 โ†’ v3.9.0](https://github.com/asottile/pyupgrade/compare/v3.8.0...v3.9.0) * Update .pre-commit-config.yaml * Update .pre-commit-config.yaml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pierre Sassoulas Co-authored-by: Bruno Oliveira --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d85abe263..4bed87035 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ repos: - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/blacken-docs - rev: 1.14.0 + rev: 1.15.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.3.0] + additional_dependencies: [black==23.7.0] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: @@ -42,7 +42,7 @@ repos: - id: reorder-python-imports args: ['--application-directories=.:src', --py38-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + rev: v3.9.0 hooks: - id: pyupgrade args: [--py38-plus] From b41acaea1203abd25b83699e12124fed06fa9f9f Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 12 Jun 2023 22:30:06 +0300 Subject: [PATCH 53/53] Switch to new-style pluggy hook wrappers Fix #11122. --- changelog/11122.improvement.rst | 6 +++ doc/en/example/simple.rst | 14 +++--- doc/en/how-to/writing_hook_functions.rst | 57 ++++++++++++++---------- doc/en/requirements.txt | 2 +- setup.cfg | 2 +- src/_pytest/assertion/__init__.py | 13 +++--- src/_pytest/cacheprovider.py | 33 +++++++------- src/_pytest/capture.py | 49 +++++++++++--------- src/_pytest/config/__init__.py | 12 ++--- src/_pytest/debugging.py | 6 +-- src/_pytest/faulthandler.py | 8 ++-- src/_pytest/helpconfig.py | 10 +++-- src/_pytest/hookspec.py | 8 ++-- src/_pytest/logging.py | 44 +++++++++--------- src/_pytest/pytester.py | 44 +++++++++--------- src/_pytest/reports.py | 3 ++ src/_pytest/setuponly.py | 32 ++++++------- src/_pytest/skipping.py | 26 ++++++----- src/_pytest/terminal.py | 23 +++++----- src/_pytest/threadexception.py | 32 +++++++------ src/_pytest/tmpdir.py | 16 ++++--- src/_pytest/unittest.py | 13 +++--- src/_pytest/unraisableexception.py | 36 ++++++++------- src/_pytest/warnings.py | 45 ++++++++++--------- testing/conftest.py | 9 ++-- testing/logging/test_reporting.py | 4 +- testing/python/collect.py | 19 ++++---- testing/test_collection.py | 7 ++- testing/test_config.py | 2 +- testing/test_mark.py | 6 +-- testing/test_python_path.py | 12 ++--- testing/test_runner.py | 10 ++--- testing/test_terminal.py | 4 +- testing/test_unittest.py | 2 +- 34 files changed, 334 insertions(+), 275 deletions(-) create mode 100644 changelog/11122.improvement.rst diff --git a/changelog/11122.improvement.rst b/changelog/11122.improvement.rst new file mode 100644 index 000000000..dedaa7d08 --- /dev/null +++ b/changelog/11122.improvement.rst @@ -0,0 +1,6 @@ +``pluggy>=1.2.0`` is now required. + +pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0. +See `pluggy's 1.2.0 changelog `_ and the :ref:`updated docs ` for details. + +Plugins which want to use new-style wrappers can do so if they require this version of pytest or later. diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 32e5188b7..5648aa383 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -808,11 +808,10 @@ case we just write some information out to a ``failures`` file: import pytest - @pytest.hookimpl(tryfirst=True, hookwrapper=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() + rep = yield # we only look at actual failing test calls, not setup/teardown if rep.when == "call" and rep.failed: @@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file: f.write(rep.nodeid + extra + "\n") + return rep + if you then have failing tests: @@ -899,16 +900,17 @@ here is a little example implemented via a local plugin: phase_report_key = StashKey[Dict[str, CollectReport]]() - @pytest.hookimpl(tryfirst=True, hookwrapper=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() + rep = yield # store test results for each phase of a call, which can # be "setup", "call", "teardown" item.stash.setdefault(phase_report_key, {})[rep.when] = rep + return rep + @pytest.fixture def something(request): diff --git a/doc/en/how-to/writing_hook_functions.rst b/doc/en/how-to/writing_hook_functions.rst index 71379016f..527aeec81 100644 --- a/doc/en/how-to/writing_hook_functions.rst +++ b/doc/en/how-to/writing_hook_functions.rst @@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case. .. _`hookwrapper`: -hookwrapper: executing around other hooks +hook wrappers: executing around other hooks ------------------------------------------------- .. currentmodule:: _pytest.core @@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes hook wrappers and passes the same arguments as to the regular hooks. At the yield point of the hook wrapper pytest will execute the next hook -implementations and return their result to the yield point in the form of -a :py:class:`Result ` instance which encapsulates a result or -exception info. The yield point itself will thus typically not raise -exceptions (unless there are bugs). +implementations and return their result to the yield point, or will +propagate an exception if they raised. Here is an example definition of a hook wrapper: @@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper: import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() - outcome = yield - # outcome.excinfo may be None or a (cls, val, tb) tuple + # If the outcome is an exception, will raise the exception. + res = yield - res = outcome.get_result() # will raise if outcome was exception + new_res = post_process_result(res) - post_process_result(res) + # Override the return value to the plugin system. + return new_res - outcome.force_result(new_res) # to override the return value to the plugin system +The hook wrapper needs to return a result for the hook, or raise an exception. -Note that hook wrappers don't return results themselves, they merely -perform tracing or other side effects around the actual hook implementations. -If the result of the underlying hook is a mutable object, they may modify -that result but it's probably better to avoid it. +In many cases, the wrapper only needs to perform tracing or other side effects +around the actual hook implementations, in which case it can return the result +value of the ``yield``. The simplest (though useless) hook wrapper is +``return (yield)``. + +In other cases, the wrapper wants the adjust or adapt the result, in which case +it can return a new value. If the result of the underlying hook is a mutable +object, the wrapper may modify that result, but it's probably better to avoid it. + +If the hook implementation failed with an exception, the wrapper can handle that +exception using a ``try-catch-finally`` around the ``yield``, by propagating it, +supressing it, or raising a different exception entirely. For more information, consult the -:ref:`pluggy documentation about hookwrappers `. +:ref:`pluggy documentation about hook wrappers `. .. _plugin-hookorder: @@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions: # Plugin 3 - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(items): # will execute even before the tryfirst one above! - outcome = yield - # will execute after all non-hookwrappers executed + try: + return (yield) + finally: + # will execute after all non-wrappers executed + ... Here is the order of execution: @@ -149,12 +159,11 @@ Here is the order of execution: Plugin1). 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield - point. The yield receives a :py:class:`Result ` instance which encapsulates - the result from calling the non-wrappers. Wrappers shall not modify the result. + point. The yield receives the result from calling the non-wrappers, or raises + an exception if the non-wrappers raised. -It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with -``hookwrapper=True`` in which case it will influence the ordering of hookwrappers -among each other. +It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers +in which case it will influence the ordering of hook wrappers among each other. Declaring new hooks diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index b6059723c..0ee999e0f 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,5 +1,5 @@ pallets-sphinx-themes -pluggy>=1.0 +pluggy>=1.2.0 pygments-pytest>=2.3.0 sphinx-removed-in>=0.2.0 sphinx>=5,<6 diff --git a/setup.cfg b/setup.cfg index f80665f42..945635369 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ py_modules = py install_requires = iniconfig packaging - pluggy>=0.12,<2.0 + pluggy>=1.2.0,<2.0 colorama;sys_platform=="win32" exceptiongroup>=1.0.0rc8;python_version<"3.11" tomli>=1.0.0;python_version<"3.11" diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index a46e58136..64ad4b0e6 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None: assertstate.hook.set_session(session) -@hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. The rewrite module will use util._reprcompare if it exists to use custom @@ -162,10 +162,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: util._assertion_pass = call_assertion_pass_hook - yield - - util._reprcompare, util._assertion_pass = saved_assert_hooks - util._config = None + try: + return (yield) + finally: + util._reprcompare, util._assertion_pass = saved_assert_hooks + util._config = None def pytest_sessionfinish(session: "Session") -> None: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index a0029d6a0..f519c974b 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -217,12 +217,12 @@ class LFPluginCollWrapper: self.lfplugin = lfplugin self._collected_at_least_one_failure = False - @hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: nodes.Collector): + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Generator[None, CollectReport, CollectReport]: + res = yield if isinstance(collector, (Session, Package)): - out = yield - res: CollectReport = out.get_result() - # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths @@ -240,19 +240,16 @@ class LFPluginCollWrapper: key=sort_key, reverse=True, ) - return elif isinstance(collector, File): if collector.path in self.lfplugin._last_failed_paths: - out = yield - res = out.get_result() result = res.result lastfailed = self.lfplugin.lastfailed # Only filter with known failures. if not self._collected_at_least_one_failure: if not any(x.nodeid in lastfailed for x in result): - return + return res self.lfplugin.config.pluginmanager.register( LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" ) @@ -268,8 +265,8 @@ class LFPluginCollWrapper: # Keep all sub-collectors. or isinstance(x, nodes.Collector) ] - return - yield + + return res class LFPluginCollSkipfiles: @@ -342,14 +339,14 @@ class LFPlugin: else: self.lastfailed[report.nodeid] = True - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] ) -> Generator[None, None, None]: - yield + res = yield if not self.active: - return + return res if self.lastfailed: previously_failed = [] @@ -394,6 +391,8 @@ class LFPlugin: else: self._report_status += "not deselecting items." + return res + def pytest_sessionfinish(self, session: Session) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "workerinput"): @@ -414,11 +413,11 @@ class NFPlugin: assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, items: List[nodes.Item] ) -> Generator[None, None, None]: - yield + res = yield if self.active: new_items: Dict[str, nodes.Item] = {} @@ -436,6 +435,8 @@ class NFPlugin: else: self.cached_nodeids.update(item.nodeid for item in items) + return res + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return] diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 5c62cf54d..81b8bffbc 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -36,6 +36,7 @@ from _pytest.fixtures import SubRequest from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item +from _pytest.reports import CollectReport _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] @@ -130,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None: sys.stderr = _reopen_stdio(sys.stderr, "wb") -@hookimpl(hookwrapper=True) -def pytest_load_initial_conftests(early_config: Config): +@hookimpl(wrapper=True) +def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]: ns = early_config.known_args_namespace if ns.capture == "fd": _windowsconsoleio_workaround(sys.stdout) @@ -145,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config): # Finally trigger conftest loading but while capturing (issue #93). capman.start_global_capturing() - outcome = yield - capman.suspend_global_capture() - if outcome.excinfo is not None: + try: + try: + yield + finally: + capman.suspend_global_capture() + except BaseException: out, err = capman.read_global_capture() sys.stdout.write(out) sys.stderr.write(err) + raise # IO Helpers. @@ -841,41 +846,45 @@ class CaptureManager: self.deactivate_fixture() self.suspend_global_capture(in_=False) - out, err = self.read_global_capture() - item.add_report_section(when, "stdout", out) - item.add_report_section(when, "stderr", err) + out, err = self.read_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) # Hooks - @hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: Collector): + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: Collector + ) -> Generator[None, CollectReport, CollectReport]: if isinstance(collector, File): self.resume_global_capture() - outcome = yield - self.suspend_global_capture() + try: + rep = yield + finally: + self.suspend_global_capture() out, err = self.read_global_capture() - rep = outcome.get_result() if out: rep.sections.append(("Captured stdout", out)) if err: rep.sections.append(("Captured stderr", err)) else: - yield + rep = yield + return rep - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): - yield + return (yield) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): - yield + return (yield) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: with self.item_capture("teardown", item): - yield + return (yield) @hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self) -> None: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3e38594a8..1f4670a56 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1341,12 +1341,14 @@ class Config: else: raise - @hookimpl(hookwrapper=True) - def pytest_collection(self) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_collection(self) -> Generator[None, object, object]: # Validate invalid ini keys after collection is done so we take in account # options added by late-loading conftest files. - yield - self._validate_config_options() + try: + return (yield) + finally: + self._validate_config_options() def _checkversion(self) -> None: import pytest @@ -1448,7 +1450,7 @@ class Config: """Issue and handle a warning during the "configure" stage. During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` - function because it is not possible to have hookwrappers around ``pytest_configure``. + function because it is not possible to have hook wrappers around ``pytest_configure``. This function is mainly intended for plugins that need to issue warnings during ``pytest_configure`` (or similar stages). diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index a3f80802c..69ec58c5b 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -304,10 +304,10 @@ class PdbInvoke: class PdbTrace: - @hookimpl(hookwrapper=True) - def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]: wrap_pytest_function_for_tracing(pyfuncitem) - yield + return (yield) def wrap_pytest_function_for_tracing(pyfuncitem): diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index af879aa44..2dc672c8d 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -62,8 +62,8 @@ def get_timeout_config_value(config: Config) -> float: return float(config.getini("faulthandler_timeout") or 0.0) -@pytest.hookimpl(hookwrapper=True, trylast=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, trylast=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: timeout = get_timeout_config_value(item.config) if timeout > 0: import faulthandler @@ -71,11 +71,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: stderr = item.config.stash[fault_handler_stderr_fd_key] faulthandler.dump_traceback_later(timeout, file=stderr) try: - yield + return (yield) finally: faulthandler.cancel_dump_traceback_later() else: - yield + return (yield) @pytest.hookimpl(tryfirst=True) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 430870608..4122d6009 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -2,6 +2,7 @@ import os import sys from argparse import Action +from typing import Generator from typing import List from typing import Optional from typing import Union @@ -97,10 +98,9 @@ def pytest_addoption(parser: Parser) -> None: ) -@pytest.hookimpl(hookwrapper=True) -def pytest_cmdline_parse(): - outcome = yield - config: Config = outcome.get_result() +@pytest.hookimpl(wrapper=True) +def pytest_cmdline_parse() -> Generator[None, Config, Config]: + config = yield if config.option.debug: # --debug | --debug was provided. @@ -128,6 +128,8 @@ def pytest_cmdline_parse(): config.add_cleanup(unset_tracing) + return config + def showversion(config: Config) -> None: if config.option.version > 1: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1f7c368f7..b01f8ec16 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -60,7 +60,7 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. """ @@ -74,7 +74,7 @@ def pytest_plugin_registered( :param pytest.PytestPluginManager manager: pytest plugin manager. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. """ @@ -113,7 +113,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> attribute or can be retrieved as the ``pytestconfig`` fixture. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. """ @@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None: imported. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. :param pytest.Config config: The pytest config object. """ diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index ea856837c..873eb5e4e 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -738,27 +738,26 @@ class LoggingPlugin: return True - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_sessionstart(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionstart") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) - @hookimpl(hookwrapper=True) - def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]: if session.config.option.collectonly: - yield - return + return (yield) if self._log_cli_enabled() and self._config.getoption("verbose") < 1: # The verbose flag is needed to avoid messy test progress output. @@ -766,7 +765,7 @@ class LoggingPlugin: with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield # Run all the tests. + return (yield) # Run all the tests. @hookimpl def pytest_runtest_logstart(self) -> None: @@ -791,12 +790,13 @@ class LoggingPlugin: item.stash[caplog_records_key][when] = caplog_handler.records item.stash[caplog_handler_key] = caplog_handler - yield + try: + yield + finally: + log = report_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) - log = report_handler.stream.getvalue().strip() - item.add_report_section(when, "log", log) - - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") @@ -804,31 +804,33 @@ class LoggingPlugin: item.stash[caplog_records_key] = empty yield from self._runtest_for(item, "setup") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("call") yield from self._runtest_for(item, "call") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("teardown") - yield from self._runtest_for(item, "teardown") - del item.stash[caplog_records_key] - del item.stash[caplog_handler_key] + try: + yield from self._runtest_for(item, "teardown") + finally: + del item.stash[caplog_records_key] + del item.stash[caplog_handler_key] @hookimpl def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_sessionfinish(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionfinish") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) @hookimpl def pytest_unconfigure(self) -> None: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d8dd3b9de..b112e6e70 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -160,29 +160,31 @@ class LsofFdLeakChecker: else: return True - @hookimpl(hookwrapper=True, tryfirst=True) - def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: + @hookimpl(wrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]: lines1 = self.get_open_files() - yield - if hasattr(sys, "pypy_version_info"): - gc.collect() - lines2 = self.get_open_files() + try: + return (yield) + finally: + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() - new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} - leaked_files = [t for t in lines2 if t[0] in new_fds] - if leaked_files: - error = [ - "***** %s FD leakage detected" % len(leaked_files), - *(str(f) for f in leaked_files), - "*** Before:", - *(str(f) for f in lines1), - "*** After:", - *(str(f) for f in lines2), - "***** %s FD leakage detected" % len(leaked_files), - "*** function %s:%s: %s " % item.location, - "See issue #2366", - ] - item.warn(PytestWarning("\n".join(error))) + new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [ + "***** %s FD leakage detected" % len(leaked_files), + *(str(f) for f in leaked_files), + "*** Before:", + *(str(f) for f in lines1), + "*** After:", + *(str(f) for f in lines2), + "***** %s FD leakage detected" % len(leaked_files), + "*** function %s:%s: %s " % item.location, + "See issue #2366", + ] + item.warn(PytestWarning("\n".join(error))) # used at least by pytest-xdist plugin diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 0a4044ec6..d09a273fc 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -249,6 +249,9 @@ class TestReport(BaseReport): """ __test__ = False + # Defined by skipping plugin. + # xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish. + wasxfail: str def __init__( self, diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 583590d6b..0f8be899a 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -28,24 +28,26 @@ def pytest_addoption(parser: Parser) -> None: ) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest -) -> Generator[None, None, None]: - yield - if request.config.option.setupshow: - if hasattr(request, "param"): - # Save the fixture parameter so ._show_fixture_action() can - # display it now and during the teardown (in .finish()). - if fixturedef.ids: - if callable(fixturedef.ids): - param = fixturedef.ids(request.param) +) -> Generator[None, object, object]: + try: + return (yield) + finally: + if request.config.option.setupshow: + if hasattr(request, "param"): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): + param = fixturedef.ids(request.param) + else: + param = fixturedef.ids[request.param_index] else: - param = fixturedef.ids[request.param_index] - else: - param = request.param - fixturedef.cached_param = param # type: ignore[attr-defined] - _show_fixture_action(fixturedef, "SETUP") + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] + _show_fixture_action(fixturedef, "SETUP") def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 26ce73758..0c5c38f5f 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -19,6 +19,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.reports import BaseReport +from _pytest.reports import TestReport from _pytest.runner import CallInfo from _pytest.stash import StashKey @@ -243,7 +244,7 @@ def pytest_runtest_setup(item: Item) -> None: xfail("[NOTRUN] " + xfailed.reason) -@hookimpl(hookwrapper=True) +@hookimpl(wrapper=True) def pytest_runtest_call(item: Item) -> Generator[None, None, None]: xfailed = item.stash.get(xfailed_key, None) if xfailed is None: @@ -252,18 +253,20 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]: if xfailed and not item.config.option.runxfail and not xfailed.run: xfail("[NOTRUN] " + xfailed.reason) - yield - - # The test run may have added an xfail mark dynamically. - xfailed = item.stash.get(xfailed_key, None) - if xfailed is None: - item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + try: + return (yield) + finally: + # The test run may have added an xfail mark dynamically. + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) -@hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item: Item, call: CallInfo[None]): - outcome = yield - rep = outcome.get_result() +@hookimpl(wrapper=True) +def pytest_runtest_makereport( + item: Item, call: CallInfo[None] +) -> Generator[None, TestReport, TestReport]: + rep = yield xfailed = item.stash.get(xfailed_key, None) if item.config.option.runxfail: pass # don't interfere @@ -286,6 +289,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): else: rep.outcome = "passed" rep.wasxfail = xfailed.reason + return rep def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 089314c3e..70453f306 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -15,7 +15,6 @@ from functools import partial from pathlib import Path from typing import Any from typing import Callable -from typing import cast from typing import ClassVar from typing import Dict from typing import final @@ -849,12 +848,11 @@ class TerminalReporter: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_sessionfinish( self, session: "Session", exitstatus: Union[int, ExitCode] - ): - outcome = yield - outcome.get_result() + ) -> Generator[None, None, None]: + result = yield self._tw.line("") summary_exit_codes = ( ExitCode.OK, @@ -875,17 +873,20 @@ class TerminalReporter: elif session.shouldstop: self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() + return result - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_terminal_summary(self) -> Generator[None, None, None]: self.summary_errors() self.summary_failures() self.summary_warnings() self.summary_passes() - yield - self.short_test_summary() - # Display any extra warnings from teardown here (if any). - self.summary_warnings() + try: + return (yield) + finally: + self.short_test_summary() + # Display any extra warnings from teardown here (if any). + self.summary_warnings() def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) @@ -1466,7 +1467,7 @@ def _get_raw_skip_reason(report: TestReport) -> str: The string is just the part given by the user. """ if hasattr(report, "wasxfail"): - reason = cast(str, report.wasxfail) + reason = report.wasxfail if reason.startswith("reason: "): reason = reason[len("reason: ") :] return reason diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index 43341e739..0b5902d66 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -59,30 +59,34 @@ class catch_threading_exception: def thread_exception_runtest_hook() -> Generator[None, None, None]: with catch_threading_exception() as cm: - yield - if cm.args: - thread_name = "" if cm.args.thread is None else cm.args.thread.name - msg = f"Exception in thread {thread_name}\n\n" - msg += "".join( - traceback.format_exception( - cm.args.exc_type, - cm.args.exc_value, - cm.args.exc_traceback, + try: + yield + finally: + if cm.args: + thread_name = ( + "" if cm.args.thread is None else cm.args.thread.name ) - ) - warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) + msg = f"Exception in thread {thread_name}\n\n" + msg += "".join( + traceback.format_exception( + cm.args.exc_type, + cm.args.exc_value, + cm.args.exc_traceback, + ) + ) + warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) -@pytest.hookimpl(hookwrapper=True, trylast=True) +@pytest.hookimpl(wrapper=True, trylast=True) def pytest_runtest_setup() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_call() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_teardown() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index fe0855c18..5473d95e1 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -28,7 +28,7 @@ from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch from _pytest.nodes import Item -from _pytest.reports import CollectReport +from _pytest.reports import TestReport from _pytest.stash import StashKey tmppath_result_key = StashKey[Dict[str, bool]]() @@ -309,10 +309,12 @@ def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]): cleanup_dead_symlinks(basetemp) -@hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item: Item, call): - outcome = yield - result: CollectReport = outcome.get_result() - +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport( + item: Item, call +) -> Generator[None, TestReport, TestReport]: + rep = yield + assert rep.when is not None empty: Dict[str, bool] = {} - item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed + item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed + return rep diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index d42a12a3a..27598cbde 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -376,8 +376,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: # Twisted trial support. -@hookimpl(hookwrapper=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@hookimpl(wrapper=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: ut: Any = sys.modules["twisted.python.failure"] Failure__init__ = ut.Failure.__init__ @@ -400,10 +400,13 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: Failure__init__(self, exc_value, exc_type, exc_tb) ut.Failure.__init__ = excstore - yield - ut.Failure.__init__ = Failure__init__ + try: + res = yield + finally: + ut.Failure.__init__ = Failure__init__ else: - yield + res = yield + return res def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py index fcb5d8237..8c0a2d9ae 100644 --- a/src/_pytest/unraisableexception.py +++ b/src/_pytest/unraisableexception.py @@ -61,33 +61,35 @@ class catch_unraisable_exception: def unraisable_exception_runtest_hook() -> Generator[None, None, None]: with catch_unraisable_exception() as cm: - yield - if cm.unraisable: - if cm.unraisable.err_msg is not None: - err_msg = cm.unraisable.err_msg - else: - err_msg = "Exception ignored in" - msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" - msg += "".join( - traceback.format_exception( - cm.unraisable.exc_type, - cm.unraisable.exc_value, - cm.unraisable.exc_traceback, + try: + yield + finally: + if cm.unraisable: + if cm.unraisable.err_msg is not None: + err_msg = cm.unraisable.err_msg + else: + err_msg = "Exception ignored in" + msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" + msg += "".join( + traceback.format_exception( + cm.unraisable.exc_type, + cm.unraisable.exc_value, + cm.unraisable.exc_traceback, + ) ) - ) - warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) + warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_setup() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_call() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_teardown() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index bb293ec08..a6af49f91 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -60,17 +60,18 @@ def catch_warnings_for_item( for arg in mark.args: warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) - yield - - for warning_message in log: - ihook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=warning_message, - nodeid=nodeid, - when=when, - location=None, + try: + yield + finally: + for warning_message in log: + ihook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) ) - ) def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: @@ -103,24 +104,24 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: return msg -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: with catch_warnings_for_item( config=item.config, ihook=item.ihook, when="runtest", item=item ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection(session: Session) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection(session: Session) -> Generator[None, object, object]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="collect", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_terminal_summary( terminalreporter: TerminalReporter, ) -> Generator[None, None, None]: @@ -128,23 +129,23 @@ def pytest_terminal_summary( with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_load_initial_conftests( early_config: "Config", ) -> Generator[None, None, None]: with catch_warnings_for_item( config=early_config, ihook=early_config.hook, when="config", item=None ): - yield + return (yield) diff --git a/testing/conftest.py b/testing/conftest.py index 8e77fcae5..926a1d5d3 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,6 +1,7 @@ import dataclasses import re import sys +from typing import Generator from typing import List import pytest @@ -21,11 +22,11 @@ if sys.gettrace(): sys.settrace(orig_trace) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection_modifyitems(items): +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection_modifyitems(items) -> Generator[None, None, None]: """Prefer faster tests. - Use a hookwrapper to do this in the beginning, so e.g. --ff still works + Use a hook wrapper to do this in the beginning, so e.g. --ff still works correctly. """ fast_items = [] @@ -62,7 +63,7 @@ def pytest_collection_modifyitems(items): items[:] = fast_items + neutral_items + slow_items + slowest_items - yield + return (yield) @pytest.fixture diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 0c8e3fd08..8c1e4f8cc 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1040,13 +1040,13 @@ def test_log_set_path(pytester: Pytester) -> None: """ import os import pytest - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_setup(item): config = item.config logging_plugin = config.pluginmanager.get_plugin("logging-plugin") report_file = os.path.join({}, item._request.node.name) logging_plugin.set_log_path(report_file) - yield + return (yield) """.format( repr(report_dir_base) ) diff --git a/testing/python/collect.py b/testing/python/collect.py index 8de216d8f..0415c3fbe 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -827,11 +827,11 @@ class TestConftestCustomization: textwrap.dedent( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makemodule(): - outcome = yield - mod = outcome.get_result() + mod = yield mod.obj.hello = "world" + return mod """ ), encoding="utf-8", @@ -855,14 +855,13 @@ class TestConftestCustomization: textwrap.dedent( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makeitem(): - outcome = yield - if outcome.excinfo is None: - result = outcome.get_result() - if result: - for func in result: - func._some123 = "world" + result = yield + if result: + for func in result: + func._some123 = "world" + return result """ ), encoding="utf-8", diff --git a/testing/test_collection.py b/testing/test_collection.py index c370951b5..ca2e2b731 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -334,12 +334,11 @@ class TestPrunetraceback: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_make_collect_report(): - outcome = yield - rep = outcome.get_result() + rep = yield rep.headerlines += ["header1"] - outcome.force_result(rep) + return rep """ ) result = pytester.runpytest(p) diff --git a/testing/test_config.py b/testing/test_config.py index 9b3fe4af0..43561000c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1317,7 +1317,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test): hookimpls = [ ( hookimpl.function.__module__, - "wrapper" if hookimpl.hookwrapper else "nonwrapper", + "wrapper" if (hookimpl.wrapper or hookimpl.hookwrapper) else "nonwrapper", ) for hookimpl in hc.get_hookimpls() ] diff --git a/testing/test_mark.py b/testing/test_mark.py index 2767260df..7415b393e 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -806,12 +806,12 @@ class TestKeywordSelection: pytester.makepyfile( conftest=""" import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makeitem(name): - outcome = yield + item = yield if name == "TestClass": - item = outcome.get_result() item.extra_keyword_matches.add("xxx") + return item """ ) reprec = pytester.inline_run(p.parent, "-s", "-k", keyword) diff --git a/testing/test_python_path.py b/testing/test_python_path.py index e1628feb1..dfef0f3fe 100644 --- a/testing/test_python_path.py +++ b/testing/test_python_path.py @@ -85,8 +85,8 @@ def test_clean_up(pytester: Pytester) -> None: # This is tough to test behaviorally because the cleanup really runs last. # So the test make several implementation assumptions: # - Cleanup is done in pytest_unconfigure(). - # - Not a hookwrapper. - # So we can add a hookwrapper ourselves to test what it does. + # - Not a hook wrapper. + # So we can add a hook wrapper ourselves to test what it does. pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n") pytester.makepyfile(test_foo="""def test_foo(): pass""") @@ -94,12 +94,14 @@ def test_clean_up(pytester: Pytester) -> None: after: Optional[List[str]] = None class Plugin: - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_unconfigure(self) -> Generator[None, None, None]: nonlocal before, after before = sys.path.copy() - yield - after = sys.path.copy() + try: + return (yield) + finally: + after = sys.path.copy() result = pytester.runpytest_inprocess(plugins=[Plugin()]) assert result.ret == 0 diff --git a/testing/test_runner.py b/testing/test_runner.py index de3e18184..cab631ee1 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -542,10 +542,10 @@ def test_runtest_in_module_ordering(pytester: Pytester) -> None: @pytest.fixture def mylist(self, request): return request.function.mylist - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_call(self, item): try: - (yield).get_result() + yield except ValueError: pass def test_hello1(self, mylist): @@ -826,12 +826,12 @@ def test_unicode_in_longrepr(pytester: Pytester) -> None: pytester.makeconftest( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(): - outcome = yield - rep = outcome.get_result() + rep = yield if rep.when == "call": rep.longrepr = 'รค' + return rep """ ) pytester.makepyfile( diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 7c2f7c94a..596c3c67e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -725,12 +725,12 @@ class TestTerminalFunctional: ) assert result.ret == 0 - def test_deselected_with_hookwrapper(self, pytester: Pytester) -> None: + def test_deselected_with_hook_wrapper(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(config, items): yield deselected = items.pop() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 99a53e0a9..24f954051 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -952,7 +952,7 @@ def test_issue333_result_clearing(pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_call(item): yield assert 0